The complete guide to custom themes
Match your brand, build a dark mode, or design from a vibe. Slate's theming goes as deep as you want it to.
How theming works
Slate's theming operates on two layers:
- Theme Settings: High-level controls for colors, fonts, spacing, and border radius. These are configured through the Theme page in the builder and stored as part of your course data.
- Custom CSS: A full CSS editor that lets you override any visual aspect of the player. This is where you can create dramatic dark themes, gradients, glassmorphism effects, and more.
The settings layer is ideal for quick brand alignment. Custom CSS is where the real creative power lives.
Under the hood, both layers work through CSS custom properties (variables). When you set a primary color in the theme settings, Slate sets --primary-color on the player's root element. Custom CSS can override these variables and target any player element directly.
Generate a theme with AI
The fastest path to a custom Slate theme: describe the look you want, paste this prompt into Claude or ChatGPT, then import the result on the Theme page. Refine in the editor afterward.
The prompt teaches the model Slate's export schema, CSS variable system, and full selector map, so you get a theme JSON and matching Custom CSS that's ready to import.
I want you to create a custom theme for Slate (an eLearning course builder). Generate a complete theme configuration that I can import.
**My desired theme:** [Describe your theme here, e.g., "A warm, earthy dark theme with amber/gold accents, inspired by a cozy library. Use rounded corners and elegant serif headings."]
---
## OUTPUT FORMAT
Produce TWO outputs:
### 1. Theme Settings JSON
A JSON object in this exact format:
```json
{
"_slateThemeExport": true,
"exportVersion": "1.0",
"exportedAt": "[current ISO date]",
"theme": {
"primaryColor": "#HEXCOLOR",
"primaryForegroundColor": "",
"hoverColor": "#HEXCOLOR",
"outlineColor": "#HEXCOLOR or rgba()",
"outlineThickness": 1,
"borderRadius": 8,
"spacing": 16,
"headingFont": "font-id",
"bodyFont": "font-id",
"headingFontWeight": 700,
"bodyFontWeight": 400,
"customCss": "[full CSS from output 2 as a JSON string — escape newlines as \n and double-quotes as \". The string can be long; max 50,000 characters.]",
"logoUrl": "",
"navigationLayout": "classic",
"contentLayout": "standard",
"lockedNavigation": false,
"assessmentAutoscroll": true,
"showExitCourse": false,
"knowledgeChecks": {
"maxAttempts": 0,
"revealCorrectAnswer": true,
"revealAnswersPerAttempt": false,
"showFeedback": true,
"eliminateWrongOptions": false
}
}
}
```
**Field notes** (so the model fills these in correctly):
- `primaryForegroundColor` — text colour on primary buttons and active nav. Leave as empty string `""` to let Slate auto-derive (light text on dark buttons, dark text on light ones). Set explicitly only when you want a specific brand pairing.
- `logoUrl` — empty string is fine; logos are uploaded via the builder rather than embedded in theme JSON.
- `contentLayout` — `"standard"` keeps text in a comfortable reading column; `"wide"` gives images, videos, tables, and multi-column blocks edge-to-edge room with a hamburger nav. Pick `"wide"` for visually heavy or editorial-style courses.
- `knowledgeChecks` — course-wide defaults for retry behaviour and feedback on knowledge checks. `maxAttempts: 0` means unlimited.
### 2. Custom CSS
The complete CSS to paste into the Custom CSS editor.
---
## CONSTRAINTS
**Color formats:** Use hex (#XXXXXX) or rgba() only. No named colors, hsl(), or 3-digit hex.
**Value ranges:**
- outlineThickness: 0-4 (integer)
- borderRadius: 0-24
- spacing: 0-32
- Font weights: 100-900 in steps of 100
**Blocked CSS patterns (these will be stripped):**
- @import
- javascript: in url()
- expression()
- behavior:
- </style>
**Available font IDs (pick from these only):**
Sans-serif: inter, open-sans, roboto, lato, poppins, nunito, work-sans, dm-sans, source-sans-3, noto-sans, mulish, rubik
Serif: playfair-display, merriweather, lora, source-serif-pro, crimson-pro, libre-baskerville, bitter
Display: montserrat, raleway, oswald, bebas-neue, archivo, sora
Handwriting: caveat, dancing-script, pacifico
Monospace: fira-code, jetbrains-mono
---
## CSS VARIABLE SYSTEM
The player uses CSS custom properties on :root. Override these to control the theme:
**Color Scale (the foundation - invert for dark themes):**
--slate-50 through --slate-900
Default light values: #F8FAFC, #F1F5F9, #E2E8F0, #CBD5E1, #94A3B8, #64748B, #475569, #334155, #1E293B, #0F172A
**Semantic colors:**
--primary-color, --primary-foreground, --accent-color, --accent-hover, --accent-light
--outline-color, --outline-thickness
--success / --success-light, --error / --error-light
`--primary-foreground` is the text colour drawn on top of `--primary-color` (buttons, active nav). Slate auto-derives a readable colour from luminance when `primaryForegroundColor` is left empty.
**Typography:**
--font-family-heading / --font-family-body
--font-weight-heading / --font-weight-body
**Spacing:** --space-1 (0.25rem) through --space-16 (4rem)
**Border radius:** --radius-sm through --radius-2xl, --radius-full
---
## TARGETABLE SELECTORS
### Shell
body, #slate-player, #player-content
### Header
#player-header, #course-title, #progress-bar, #progress-fill, #progress-text
### Navigation Sidebar
#player-nav, .nav-header, .nav-header-title, .nav-section-title
.nav-lesson, .nav-lesson:hover, .nav-lesson.active, .nav-lesson.viewed:not(.active)::after
### Footer
#player-footer, #btn-prev, #btn-next
### Text Content
.block-text, .block-text h1/h2/h3/h4, .block-text a, .block-text code, .block-text blockquote
### Buttons
.button-primary/.slate-button.button-primary, .button-secondary, .button-outline (+ :hover)
### Media
.block-image figure / img / figcaption
.block-video .video-wrapper, .block-video .video-caption
.audio-block, .audio-block audio, .audio-block figcaption (note: the audio block's class is "audio-block", NOT "block-audio")
.block-iframe .iframe-wrapper
### Interactive Blocks
.block-accordion, .block-tabs, .tab-button.active
.card, .card-style-*, .flip-card-face, .carousel-card, .carousel-dot.active
### Tables
.slate-table th/td, .slate-table thead th, .slate-table.table-stripe-*
### Knowledge Checks
.kc-option, .kc-option.selected, .kc-option.correct/.incorrect, .kc-submit, .kc-feedback
### Assessment
.assessment-intro, .assessment-results, .assessment-results.passed/.failed
.assessment-score-circle, .assessment-start-btn, .assessment-submit-btn
---
## TIPS FOR GREAT THEMES
1. Dark themes: Invert the --slate-* scale (50=darkest, 900=lightest)
2. Use var() references instead of hardcoding colors
3. Gradients on #progress-fill, #course-title, .button-primary, .nav-lesson.active
4. Glassmorphism: backdrop-filter: blur(12px) + rgba() backgrounds on header/footer
5. Glow effects: box-shadow: 0 0 Npx rgba(accent, 0.3-0.5) on active elements
6. Gradient text: background: linear-gradient(...), -webkit-background-clip: text
7. Always style both .button-primary AND .slate-button.button-primary
8. Match scrollbar styling to your color scheme
9. Test all block types: accordions, tabs, knowledge checks, cards, tables, assessments
Now generate the theme based on my description above..json file and import from Theme > Import.Theme settings reference
These properties are configured through the Slate builder's Theme page. They're also included when you export/import a theme JSON file.
Colors
| Property | Type | Range | Default | Description |
|---|---|---|---|---|
primaryColor | Hex or RGBA | Any valid color | #18181B | Primary brand color for buttons, active states, accents |
primaryForegroundColor | Hex or RGBA | Any valid color, or empty | Auto-derived | Text colour on primary buttons and active nav items. Leave empty to auto-derive from luminance (light text on dark, dark on light) |
hoverColor | Hex or RGBA | Any valid color | Auto-derived | Hover state color. Leave empty for automatic derivation (10% darker) |
outlineColor | Hex or RGBA | Any valid color | #E4E4E7 | Border color for buttons and UI elements |
outlineThickness | Number | 0-4 px | 1 | Border width for buttons and outlined elements |
Color format examples:
#3B82F6 (hex) #18181B (hex) rgba(139, 92, 246, 0.3) (rgba with alpha) rgb(59, 130, 246) (rgb)
Typography
| Property | Type | Range | Default | Description |
|---|---|---|---|---|
headingFont | String | Font ID | inter | Font used for headings (h1-h4, lesson titles) |
bodyFont | String | Font ID | inter | Font used for body text and UI elements |
headingFontWeight | Number | 100-900 | 700 | Font weight for headings |
bodyFontWeight | Number | 100-900 | 400 | Font weight for body text |
Layout
| Property | Type | Range | Default | Description |
|---|---|---|---|---|
borderRadius | Number | 0-24 px | 6 | Controls how round UI elements are (0 = sharp, 24 = very round) |
spacing | Number | 0-32 px | 16 | Controls padding and gap scaling throughout the player |
contentLayout | String | standard or wide | standard | wide gives images, videos, tables, and multi-column blocks edge-to-edge room while text stays in a comfortable reading column. Picks up a hamburger nav for a modern, editorial feel |
Navigation & UI
| Property | Type | Options | Default | Description |
|---|---|---|---|---|
navigationLayout | String | classic or vertical | classic | Classic = footer bar with prev/next. Vertical = inline next button |
showScrollIndicator | Boolean | true/false | false | Shows a scroll-down indicator when content extends below the fold |
enableSearch | Boolean | true/false | true | Enables the search box in the player sidebar |
lockedNavigation | Boolean | true/false | false | Requires learners to complete lessons in sequence |
assessmentAutoscroll | Boolean | true/false | true | Whether assessments scroll to the next question automatically after each answer. Toggle off if you want learners to scroll manually |
showExitCourse | Boolean | true/false | false | Shows an explicit “Exit course” control in the player chrome |
Knowledge check defaults
Course-wide defaults for knowledge check behaviour, configured in Theme > Knowledge Checks. Individual questions can still override these. Stored as a knowledgeChecks object on the theme.
| Property | Type | Description |
|---|---|---|
maxAttempts | Integer ≥ 0 | How many times a learner can retry a question. 0 means unlimited |
revealCorrectAnswer | Boolean | Show the correct answer after the learner exhausts their attempts |
revealAnswersPerAttempt | Boolean | Reveal which options are right or wrong after every attempt, not only the final one |
showFeedback | Boolean | Show the correct/incorrect feedback message after submission |
eliminateWrongOptions | Boolean | Multiple-choice only: previously-wrong options stay marked and click-disabled across attempts |
CSS custom properties reference
These CSS variables are defined on the player's :root element. You can override any of them in Custom CSS.
Color scale
The slate color scale is the backbone of the player's visual hierarchy. In light themes, --slate-50 is the lightest and --slate-900 is the darkest. To create a dark theme, you invert this scale so that --slate-50 becomes dark and --slate-900 becomes light.
| Variable | Default (Light) | Used for |
|---|---|---|
--slate-50 | #F8FAFC | Page backgrounds, lightest surfaces |
--slate-100 | #F1F5F9 | Card backgrounds, subtle surfaces |
--slate-200 | #E2E8F0 | Borders, dividers, secondary backgrounds |
--slate-300 | #CBD5E1 | Heavier borders, disabled states |
--slate-400 | #94A3B8 | Placeholder text, icons |
--slate-500 | #64748B | Secondary text, captions |
--slate-600 | #475569 | Body text (in dark themes) |
--slate-700 | #334155 | Primary body text |
--slate-800 | #1E293B | Emphasized text, headings |
--slate-900 | #0F172A | Strongest text, titles |
Semantic colors
| Variable | Default | Description |
|---|---|---|
--primary-color | #000000 | Primary brand color (set by primaryColor setting) |
--primary-foreground | Auto-derived | Text colour drawn on top of --primary-color (buttons, active nav). Set at runtime from primaryForegroundColor, or computed from primaryColor luminance when that setting is empty |
--accent-color | #000000 | Alias for primary color (backwards compatibility) |
--accent-hover | #333333 | Hover/active state (auto-derived or set by hoverColor) |
--accent-light | rgba(0, 0, 0, 0.1) | Light tint of primary (~15% opacity) |
--outline-color | #E2E8F0 | Button/element border color |
--outline-thickness | 1px | Button/element border width |
--success | #10B981 | Success states (correct answers, completion) |
--success-light | rgba(16,185,129,0.1) | Success background tint |
--error | #EF4444 | Error states (incorrect answers) |
--error-light | rgba(239,68,68,0.1) | Error background tint |
Typography
| Variable | Default | Description |
|---|---|---|
--font-family | 'Inter', system-ui, sans-serif | Base font stack |
--font-family-heading | var(--font-family) | Heading font |
--font-family-body | var(--font-family) | Body font |
--font-weight-heading | 700 | Heading weight |
--font-weight-body | 400 | Body weight |
--text-xs to --text-4xl | 0.75rem to 2.25rem | Type scale (xs, sm, base, lg, xl, 2xl, 3xl, 4xl) |
--leading-tight | 1.25 | Tight line height |
--leading-normal | 1.5 | Normal line height |
--leading-relaxed | 1.625 | Relaxed line height |
Spacing
| Variable | Default | Description |
|---|---|---|
--spacing-base | 16px | Base spacing unit (set by spacing setting) |
--space-1 | 0.25rem | 4px |
--space-2 | 0.5rem | 8px |
--space-3 | 0.75rem | 12px |
--space-4 | 1rem | 16px |
--space-6 | 1.5rem | 24px |
--space-8 | 2rem | 32px |
--space-12 | 3rem | 48px |
--space-16 | 4rem | 64px |
Border radius
| Variable | Default | Description |
|---|---|---|
--radius-base | 8px | Base radius (set by borderRadius setting) |
--radius-sm | 0.25rem | Small radius |
--radius-md | 0.375rem | Medium radius |
--radius-lg | 0.5rem | Large radius |
--radius-xl | 0.75rem | Extra large radius |
--radius-2xl | 1rem | 2x large radius |
--radius-full | 9999px | Full/pill radius |
Shadows
| Variable | Default |
|---|---|
--shadow-sm | 0 1px 2px 0 rgba(0,0,0,0.05) |
--shadow-md | 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1) |
--shadow-lg | 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1) |
--shadow-xl | 0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1) |
Transitions
| Variable | Default |
|---|---|
--transition-fast | 150ms ease |
--transition-base | 200ms ease |
--transition-slow | 300ms ease |
--transition-slower | 500ms ease |
--ease-out | cubic-bezier(0, 0, 0.2, 1) |
--ease-bounce | cubic-bezier(0.34, 1.56, 0.64, 1) |
Layout
| Variable | Default | Description |
|---|---|---|
--sidebar-width | 280px | Navigation sidebar width |
--header-height | 72px | Player header height |
--footer-height | 64px | Player footer height |
--content-max-width | 720px | Maximum content width |
Built-in fonts
Slate includes 30 Google Fonts organized by category. Use the Font ID when setting headingFont or bodyFont in your theme.
Sans-serif (clean, modern)
| Font ID | Name | Weights |
|---|---|---|
inter | Inter | 400, 500, 600, 700 |
open-sans | Open Sans | 400, 500, 600, 700 |
roboto | Roboto | 400, 500, 700 |
lato | Lato | 400, 700 |
poppins | Poppins | 400, 500, 600, 700 |
nunito | Nunito | 400, 600, 700 |
work-sans | Work Sans | 400, 500, 600, 700 |
dm-sans | DM Sans | 400, 500, 700 |
source-sans-3 | Source Sans 3 | 400, 500, 600, 700 |
noto-sans | Noto Sans | 400, 500, 600, 700 |
mulish | Mulish | 400, 500, 600, 700 |
rubik | Rubik | 400, 500, 600, 700 |
Serif (traditional, elegant)
| Font ID | Name | Weights |
|---|---|---|
playfair-display | Playfair Display | 400, 500, 600, 700 |
merriweather | Merriweather | 400, 700 |
lora | Lora | 400, 500, 600, 700 |
source-serif-pro | Source Serif Pro | 400, 600, 700 |
crimson-pro | Crimson Pro | 400, 500, 600, 700 |
libre-baskerville | Libre Baskerville | 400, 700 |
bitter | Bitter | 400, 500, 600, 700 |
Display (headlines, impact)
| Font ID | Name | Weights |
|---|---|---|
montserrat | Montserrat | 400, 500, 600, 700, 800 |
raleway | Raleway | 400, 500, 600, 700 |
oswald | Oswald | 400, 500, 600, 700 |
bebas-neue | Bebas Neue | 400 |
archivo | Archivo | 400, 500, 600, 700 |
sora | Sora | 400, 500, 600, 700 |
Handwriting (friendly, personal)
| Font ID | Name | Weights |
|---|---|---|
caveat | Caveat | 400, 700 |
dancing-script | Dancing Script | 400, 700 |
pacifico | Pacifico | 400 |
Monospace (code, technical)
| Font ID | Name | Weights |
|---|---|---|
fira-code | Fira Code | 400, 500, 700 |
jetbrains-mono | JetBrains Mono | 400, 500, 700 |
custom- prefix (e.g., custom-acme-sans). See the Custom Fonts section in the builder's Font settings.Custom CSS deep dive
The Custom CSS editor is where you unlock the full creative potential of Slate themes. Access it from Theme > Advanced in the builder.
How it works
Any CSS you write is injected into the player as a <style> tag. It loads after the default styles, so your rules override the defaults. You have access to all CSS custom properties and can target any player element by its class name or ID.
Security constraints
For security, the following CSS patterns are automatically blocked:
@import: no external stylesheet loadingjavascript:inurl(): no script injectionexpression(): no IE expression evaluationbehavior:: no IE behavior injection</style>: no tag breakout
Everything else is fair game.
Player structure & key selectors
Here's a map of the player's DOM structure with the CSS selectors you'll use most often.
Shell & layout
body { } /* Page background (outside the player) */
#slate-player { } /* The entire player container */
#player-content { } /* Main content area */Header
#player-header { } /* Top bar with title and progress */
#course-title { } /* Course title text */
#progress-bar { } /* Progress bar track */
#progress-fill { } /* Progress bar filled portion */
#progress-text { } /* "75% Complete" text */Navigation sidebar
#player-nav { } /* Sidebar container */
.nav-header { } /* Sidebar header area */
.nav-header-title { } /* Course name in sidebar */
.nav-header-subtitle { } /* Section subtitle */
.nav-section-title { } /* Section headings */
.nav-lesson { } /* Individual lesson links */
.nav-lesson:hover { } /* Lesson hover state */
.nav-lesson.active { } /* Currently active lesson */
.nav-lesson.viewed { } /* Previously completed lessons */
.nav-toggle { } /* Sidebar toggle button */
.nav-close { } /* Sidebar close button (mobile) */
.nav-overlay { } /* Mobile backdrop overlay */Footer
#player-footer { } /* Bottom navigation bar */
#btn-prev { } /* Previous button */
#btn-next { } /* Next button */
#btn-prev:disabled { } /* Disabled previous */
#btn-next:disabled { } /* Disabled next */Text content
.block-text { } /* Text block container */
.block-text h1 { } /* Heading 1 */
.block-text h2 { } /* Heading 2 */
.block-text h3 { } /* Heading 3 */
.block-text h4 { } /* Heading 4 */
.block-text p { } /* Paragraphs */
.block-text strong { } /* Bold text */
.block-text a { } /* Links */
.block-text a:hover { } /* Link hover */
.block-text ul, .block-text ol { } /* Lists */
.block-text code { } /* Inline code */
.block-text blockquote { } /* Blockquotes */Media blocks
.block-image figure { } /* Image wrapper */
.block-image img { } /* Image element */
.block-image figcaption { } /* Image caption */
.block-video .video-wrapper { } /* Video container — selector is scoped under .block-video for specificity */
.block-video .video-caption { } /* Video caption */
.audio-block { } /* Audio block figure (the audio block's class is .audio-block, not .block-audio) */
.audio-block audio { } /* Audio element */
.audio-block figcaption { } /* Audio caption */
.block-iframe .iframe-wrapper { } /* Embed container */Interactive blocks
/* Accordion */
.block-accordion { } /* Accordion container */
.accordion-item { } /* Individual accordion item */
.accordion-trigger { } /* Clickable header */
.accordion-trigger:hover { } /* Header hover */
.accordion-icon { } /* Expand/collapse icon */
.accordion-content { } /* Expanded content area */
/* Tabs */
.block-tabs { } /* Tabs container */
.tabs-header-wrapper { } /* Tab bar */
.tab-button { } /* Individual tab */
.tab-button:hover { } /* Tab hover */
.tab-button.active { } /* Active tab */
.tab-panel { } /* Tab content area */
/* Cards */
.card { } /* Card container */
.card-title { } /* Card title */
.card-subtitle { } /* Card subtitle */
.card-content { } /* Card body */
.card-style-default { } /* Default card variant */
.card-style-outlined { } /* Outlined card */
.card-style-elevated { } /* Elevated (shadow) card */
.card-style-filled { } /* Filled background card */
/* Flip Cards */
.flip-card-face { } /* Card face (front & back) */
.flip-card-body { } /* Card face content */
.flip-card-hint { } /* "Click to flip" hint */
/* Card Carousel */
.carousel-card { } /* Carousel card */
.carousel-card-body { } /* Carousel card content */
.carousel-nav { } /* Prev/next arrows */
.carousel-dot { } /* Pagination dot */
.carousel-dot.active { } /* Active pagination dot */Buttons
.button-primary, .slate-button.button-primary { }
.button-primary:hover, .slate-button.button-primary:hover { }
.button-secondary, .slate-button.button-secondary { }
.button-secondary:hover, .slate-button.button-secondary:hover { }
.button-outline, .slate-button.button-outline { }
.button-outline:hover, .slate-button.button-outline:hover { }Dividers
.divider-line { } /* Line divider */
.divider-dots::before { } /* Dotted divider */Tables
.block-table .table-wrapper { }
.slate-table { } /* Table element */
.slate-table th { } /* Table header cells */
.slate-table td { } /* Table body cells */
.slate-table thead th { } /* Top header row */
.slate-table th[scope="row"] { } /* Row headers */
.slate-table.table-border-all th,
.slate-table.table-border-all td { } /* All borders variant */
.slate-table.table-border-horizontal th,
.slate-table.table-border-horizontal td { } /* Horizontal borders */
.slate-table.table-stripe-even tbody tr:nth-child(even) { }
.slate-table.table-stripe-odd tbody tr:nth-child(odd) { }
.block-table figcaption { } /* Table caption */Hotspots
.hotspot-popover { } /* Popover container */
.hotspot-popover-header { } /* Popover header */
.hotspot-popover-body a { } /* Links inside popovers */
.hotspot-marker.active { } /* Active hotspot marker */Knowledge checks
.block-knowledge-check { } /* Quiz container */
.kc-question { } /* Question text */
.kc-hint { } /* Hint text */
.kc-option { } /* Answer option */
.kc-option::before { } /* Radio/checkbox indicator */
.kc-option:hover { } /* Option hover */
.kc-option.selected { } /* Selected option */
.kc-option.correct { } /* Correct answer (after submit) */
.kc-option.incorrect { } /* Incorrect answer (after submit) */
.kc-submit { } /* Submit button */
.kc-feedback.correct { } /* Correct feedback message */
.kc-feedback.incorrect { } /* Incorrect feedback message */Assessment (scored quizzes)
.assessment-intro { } /* Assessment intro card */
.assessment-intro-icon { } /* Icon container */
.assessment-intro-title { } /* Assessment title */
.assessment-detail { } /* Stats (questions, passing score) */
.assessment-start-btn { } /* Start button */
.assessment-header { } /* In-progress header */
.assessment-question-wrapper { } /* Question card */
.assessment-submit-btn { } /* Submit assessment */
.assessment-results { } /* Results card */
.assessment-results.passed { } /* Passed state */
.assessment-results.failed { } /* Failed state */
.assessment-results.locked { } /* No retries remaining */
.assessment-score-circle { } /* Score display */
.assessment-retry-btn { } /* Retry button */Scrollbar & focus
::-webkit-scrollbar { } /* Scrollbar (WebKit) */
::-webkit-scrollbar-track { } /* Scrollbar track */
::-webkit-scrollbar-thumb { } /* Scrollbar handle */
:focus-visible { } /* Keyboard focus outline */
.skip-link { } /* Skip navigation link */The dark theme technique
Creating a dark theme in Slate is straightforward: invert the slate color scale so that low numbers are dark and high numbers are light. This works because the entire player uses the --slate-* variables semantically, so flipping the scale flips the entire UI.
:root {
/* Inverted color scale: dark backgrounds, light text */
--slate-50: #18181b; /* Was near-white, now near-black */
--slate-100: #1f1f23;
--slate-200: #27272a;
--slate-300: #3f3f46;
--slate-400: #71717a;
--slate-500: #a1a1aa;
--slate-600: #d4d4d8;
--slate-700: #e4e4e7;
--slate-800: #f4f4f5;
--slate-900: #fafafa; /* Was near-black, now near-white */
}
body {
background: #09090b; /* Darkest background for the page */
}That single override transforms the entire player into dark mode. From there, you can customize accent colors, gradients, and effects to taste.
Theme presets
Once you've dialled in a look you'll want again, save it as a theme preset. A preset bundles every Theme setting plus your Custom CSS into a named entry on your account. Apply it to any course with one click, set it as the default for new courses, or share it across your team.
What a preset includes
- Every Theme setting in this guide (colours, typography, layout, navigation, knowledge-check defaults)
- Your Custom CSS, exactly as written
- Your uploaded logo reference
How to use them
- Save: on the Theme page, click Save as preset and give it a name
- Apply: open the preset picker on any course and pick one
- Update: overwrite a saved preset with your latest changes
- Import as preset: drop in a theme
.jsonfile and save it straight to your library - Team default: on Team plans, one preset can be marked as the default so new courses inherit it automatically
Example themes
Midnight Aurora
A dark theme with vibrant purple accents, gradient effects, and glowing elements. This is one of Slate's built-in themes.
{
"primaryColor": "#8B5CF6",
"hoverColor": "#7C3AED",
"outlineColor": "rgba(139, 92, 246, 0.3)",
"outlineThickness": 1,
"borderRadius": 12,
"spacing": 16
}Classic Dark
A cleaner, more professional dark theme using blue as the accent color. Uses var() references extensively, making it easy to adapt by changing the accent color variables.
{
"primaryColor": "#3B82F6",
"hoverColor": "#2563EB",
"outlineColor": "rgba(63, 63, 70, 0.8)",
"outlineThickness": 1,
"borderRadius": 8,
"spacing": 16
}Theme export/import format
Themes can be exported as JSON files and shared with others. This is the format Slate uses:
{
"_slateThemeExport": true,
"exportVersion": "1.0",
"exportedAt": "2026-05-27T12:00:00.000Z",
"courseName": "My Course",
"theme": {
"primaryColor": "#8B5CF6",
"primaryForegroundColor": "",
"hoverColor": "#7C3AED",
"outlineColor": "rgba(139, 92, 246, 0.3)",
"outlineThickness": 1,
"borderRadius": 12,
"spacing": 16,
"headingFont": "playfair-display",
"bodyFont": "inter",
"headingFontWeight": 700,
"bodyFontWeight": 400,
"customCss": "/* Your custom CSS here */",
"logoUrl": "",
"showScrollIndicator": false,
"enableSearch": true,
"navigationLayout": "classic",
"contentLayout": "standard",
"lockedNavigation": false,
"assessmentAutoscroll": true,
"showExitCourse": false,
"knowledgeChecks": {
"maxAttempts": 0,
"revealCorrectAnswer": true,
"revealAnswersPerAttempt": false,
"showFeedback": true,
"eliminateWrongOptions": false
}
}
}Required fields
_slateThemeExport (must be true), exportVersion, exportedAt, and the theme object with at minimum primaryColor, outlineColor, outlineThickness, borderRadius, spacing, headingFont, bodyFont, headingFontWeight, and bodyFontWeight. Everything else is optional and falls back to a sensible default when omitted.
Validation rules
- Colors must be valid hex (
#XXXXXX) orrgba()format hoverColorandprimaryForegroundColorcan be an empty string (auto-derived) or a valid coloroutlineThickness: integer 0-4borderRadius: 0-24spacing: 0-32- Font weights: 100-900 in steps of 100
- Custom CSS: max 50,000 characters
- Font IDs must match a built-in font or use the
custom-prefix navigationLayout:classicorverticalcontentLayout:standardorwideknowledgeChecks, when present, must include all five fields (maxAttempts,revealCorrectAnswer,revealAnswersPerAttempt,showFeedback,eliminateWrongOptions). Omit the field entirely to inherit Slate's built-ins
How to use
- Export: Go to Theme page, click the export button to download a
.jsonfile - Import: Click import and select a theme JSON file
- Share: Send the JSON file to colleagues or post it online
Step-by-step: building a custom dark theme
Let's build a dark theme from scratch with a teal accent color.
In the Slate builder, go to the Theme page and set:
- Primary Color:
#14B8A6(teal) - Hover Color: leave empty (auto-derived)
- Outline Color:
rgba(20, 184, 166, 0.3) - Outline Thickness:
1 - Border Radius:
10 - Spacing:
16
Open the Custom CSS editor and start with the inverted color scale:
/* Teal Dark Theme */
:root {
/* Inverted slate scale for dark mode */
--slate-50: #0f1419;
--slate-100: #151c22;
--slate-200: #1c252d;
--slate-300: #2a3541;
--slate-400: #5c6b7a;
--slate-500: #8899a8;
--slate-600: #b0c0cf;
--slate-700: #d0dbe5;
--slate-800: #e8eff4;
--slate-900: #f5f8fa;
/* Teal accent colors */
--accent-color: #14b8a6;
--accent-hover: #0d9488;
--accent-light: rgba(20, 184, 166, 0.15);
--primary-color: #14b8a6;
/* Status colors */
--success: #22c55e;
--success-light: rgba(34, 197, 94, 0.15);
--error: #f43f5e;
--error-light: rgba(244, 63, 94, 0.15);
--outline-color: rgba(20, 184, 166, 0.3);
--outline-thickness: 1px;
}body {
background: #0a0f13;
color: var(--slate-700);
}
#slate-player {
background: var(--slate-50);
}#player-header {
background: rgba(15, 20, 25, 0.95);
backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(20, 184, 166, 0.15);
}
#course-title { color: var(--slate-900); }
#progress-bar { background: rgba(20, 184, 166, 0.1); border: 1px solid rgba(20, 184, 166, 0.2); }
#progress-fill { background: var(--accent-color); }
#player-footer {
background: rgba(15, 20, 25, 0.95);
backdrop-filter: blur(12px);
border-top: 1px solid rgba(20, 184, 166, 0.15);
}
#btn-next { background: var(--accent-color); color: #fff; }
#btn-next:hover:not(:disabled) { background: var(--accent-hover); }
#btn-prev { background: var(--slate-200); color: var(--slate-800); border: 1px solid var(--slate-300); }#player-nav {
background: rgba(10, 15, 19, 0.98);
border-right: 1px solid rgba(20, 184, 166, 0.1);
}
.nav-header { background: var(--slate-50); border-bottom: 1px solid rgba(20, 184, 166, 0.1); }
.nav-header-title { color: var(--slate-900); }
.nav-section-title { color: #2dd4bf; }
.nav-lesson { color: var(--slate-600); border: 1px solid transparent; }
.nav-lesson:hover { background: rgba(20, 184, 166, 0.08); color: var(--slate-800); }
.nav-lesson.active { background: var(--accent-color); color: #fff; }.block-text a { color: #2dd4bf; }
.block-text a:hover { color: #5eead4; }
.block-text code { background: var(--slate-200); color: #2dd4bf; }
.block-text blockquote { border-left: 3px solid var(--accent-color); background: var(--slate-100); }
.button-primary, .slate-button.button-primary { background: var(--accent-color); color: #fff; }
.button-primary:hover, .slate-button.button-primary:hover { background: var(--accent-hover); }Once you're happy with the result, export your theme as JSON from the Theme page. You can share the file with your team, apply it to other courses, or save it as your default theme for new courses.
Tips and best practices
- Start with a built-in template that's closest to your goal, then customize from there
- Use the Theme Preview (desktop) to see changes in real-time as you edit
- Save your theme as default so all new courses start with your brand
- Export before experimenting so you can always roll back
- Test with all block types: create a test course with one of every block type to verify your theme covers everything
- Dark themes need extra attention on success/error colors. Make sure green and red are visible on dark backgrounds
- Font pairing: try a serif heading font with a sans-serif body font for elegant contrast (e.g., Playfair Display + Inter, or Merriweather + Work Sans)
- Use
var()references instead of hardcoding colors. This makes themes easier to modify later - Gradients on
#progress-fill,#course-title,.button-primary,.nav-lesson.activecreate visual interest - Glassmorphism via
backdrop-filter: blur(12px)+ semi-transparentrgba()backgrounds on header and footer - Glow effects via
box-shadow: 0 0 Npx rgba(accent, 0.3-0.5)on active elements - Always style both
.button-primaryand.slate-button.button-primaryfor full coverage - Scrollbar styling makes a big difference in dark themes. Match it to your color scheme