The complete guide to custom themes
Full control over your course's look and feel. Match your brand, build dark modes, or create something entirely unique with Slate's CSS-powered theming system.
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.
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 |
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 |
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 |
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) |
--accent-color | #000000 | Alias for primary color (backwards compatibility) |
--accent-hover | #333333 | Hover/active state (auto-derived or set by hoverColor) |
--accent-color-light | rgba(0,0,0,0.1) | Light tint of primary (auto-derived, 90% lighter) |
--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 24 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 */
.video-wrapper { } /* Video container */
.video-caption { } /* Video caption */
.block-audio figure { } /* Audio player wrapper */
.block-audio audio { } /* Audio element */
.block-audio 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.
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-02-11T12:00:00.000Z",
"courseName": "My Course",
"theme": {
"primaryColor": "#8B5CF6",
"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 */",
"showScrollIndicator": false,
"enableSearch": true,
"navigationLayout": "classic"
}
}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.
Validation rules
- Colors must be valid hex (
#XXXXXX) orrgba()format hoverColorcan 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
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.
AI prompt template
Copy this prompt and paste it into Claude, ChatGPT, or any AI assistant to generate a custom Slate theme. Replace the description at the top with your desired style.
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