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.

Midnight Aurora
Classic Dark
Teal Dark

How theming works

Slate's theming operates on two layers:

  1. 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.
  2. 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

PropertyTypeRangeDefaultDescription
primaryColorHex or RGBAAny valid color#18181BPrimary brand color for buttons, active states, accents
hoverColorHex or RGBAAny valid colorAuto-derivedHover state color. Leave empty for automatic derivation (10% darker)
outlineColorHex or RGBAAny valid color#E4E4E7Border color for buttons and UI elements
outlineThicknessNumber0-4 px1Border width for buttons and outlined elements

Color format examples:

Color formats
#3B82F6          (hex)
#18181B          (hex)
rgba(139, 92, 246, 0.3)   (rgba with alpha)
rgb(59, 130, 246)          (rgb)

Typography

PropertyTypeRangeDefaultDescription
headingFontStringFont IDinterFont used for headings (h1-h4, lesson titles)
bodyFontStringFont IDinterFont used for body text and UI elements
headingFontWeightNumber100-900700Font weight for headings
bodyFontWeightNumber100-900400Font weight for body text

Layout

PropertyTypeRangeDefaultDescription
borderRadiusNumber0-24 px6Controls how round UI elements are (0 = sharp, 24 = very round)
spacingNumber0-32 px16Controls padding and gap scaling throughout the player

Navigation & UI

PropertyTypeOptionsDefaultDescription
navigationLayoutStringclassic or verticalclassicClassic = footer bar with prev/next. Vertical = inline next button
showScrollIndicatorBooleantrue/falsefalseShows a scroll-down indicator when content extends below the fold
enableSearchBooleantrue/falsetrueEnables the search box in the player sidebar
lockedNavigationBooleantrue/falsefalseRequires 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.

VariableDefault (Light)Used for
--slate-50#F8FAFCPage backgrounds, lightest surfaces
--slate-100#F1F5F9Card backgrounds, subtle surfaces
--slate-200#E2E8F0Borders, dividers, secondary backgrounds
--slate-300#CBD5E1Heavier borders, disabled states
--slate-400#94A3B8Placeholder text, icons
--slate-500#64748BSecondary text, captions
--slate-600#475569Body text (in dark themes)
--slate-700#334155Primary body text
--slate-800#1E293BEmphasized text, headings
--slate-900#0F172AStrongest text, titles

Semantic colors

VariableDefaultDescription
--primary-color#000000Primary brand color (set by primaryColor setting)
--accent-color#000000Alias for primary color (backwards compatibility)
--accent-hover#333333Hover/active state (auto-derived or set by hoverColor)
--accent-color-lightrgba(0,0,0,0.1)Light tint of primary (auto-derived, 90% lighter)
--outline-color#E2E8F0Button/element border color
--outline-thickness1pxButton/element border width
--success#10B981Success states (correct answers, completion)
--success-lightrgba(16,185,129,0.1)Success background tint
--error#EF4444Error states (incorrect answers)
--error-lightrgba(239,68,68,0.1)Error background tint

Typography

VariableDefaultDescription
--font-family'Inter', system-ui, sans-serifBase font stack
--font-family-headingvar(--font-family)Heading font
--font-family-bodyvar(--font-family)Body font
--font-weight-heading700Heading weight
--font-weight-body400Body weight
--text-xs to --text-4xl0.75rem to 2.25remType scale (xs, sm, base, lg, xl, 2xl, 3xl, 4xl)
--leading-tight1.25Tight line height
--leading-normal1.5Normal line height
--leading-relaxed1.625Relaxed line height

Spacing

VariableDefaultDescription
--spacing-base16pxBase spacing unit (set by spacing setting)
--space-10.25rem4px
--space-20.5rem8px
--space-30.75rem12px
--space-41rem16px
--space-61.5rem24px
--space-82rem32px
--space-123rem48px
--space-164rem64px

Border radius

VariableDefaultDescription
--radius-base8pxBase radius (set by borderRadius setting)
--radius-sm0.25remSmall radius
--radius-md0.375remMedium radius
--radius-lg0.5remLarge radius
--radius-xl0.75remExtra large radius
--radius-2xl1rem2x large radius
--radius-full9999pxFull/pill radius

Shadows

VariableDefault
--shadow-sm0 1px 2px 0 rgba(0,0,0,0.05)
--shadow-md0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)
--shadow-lg0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -4px rgba(0,0,0,0.1)
--shadow-xl0 20px 25px -5px rgba(0,0,0,0.1), 0 8px 10px -6px rgba(0,0,0,0.1)

Transitions

VariableDefault
--transition-fast150ms ease
--transition-base200ms ease
--transition-slow300ms ease
--transition-slower500ms ease
--ease-outcubic-bezier(0, 0, 0.2, 1)
--ease-bouncecubic-bezier(0.34, 1.56, 0.64, 1)

Layout

VariableDefaultDescription
--sidebar-width280pxNavigation sidebar width
--header-height72pxPlayer header height
--footer-height64pxPlayer footer height
--content-max-width720pxMaximum 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 IDNameWeights
interInter400, 500, 600, 700
open-sansOpen Sans400, 500, 600, 700
robotoRoboto400, 500, 700
latoLato400, 700
poppinsPoppins400, 500, 600, 700
nunitoNunito400, 600, 700
work-sansWork Sans400, 500, 600, 700
dm-sansDM Sans400, 500, 700
source-sans-3Source Sans 3400, 500, 600, 700
noto-sansNoto Sans400, 500, 600, 700
mulishMulish400, 500, 600, 700
rubikRubik400, 500, 600, 700

Serif (traditional, elegant)

Font IDNameWeights
playfair-displayPlayfair Display400, 500, 600, 700
merriweatherMerriweather400, 700
loraLora400, 500, 600, 700
source-serif-proSource Serif Pro400, 600, 700
crimson-proCrimson Pro400, 500, 600, 700
libre-baskervilleLibre Baskerville400, 700
bitterBitter400, 500, 600, 700

Display (headlines, impact)

Font IDNameWeights
montserratMontserrat400, 500, 600, 700, 800
ralewayRaleway400, 500, 600, 700
oswaldOswald400, 500, 600, 700
bebas-neueBebas Neue400
archivoArchivo400, 500, 600, 700
soraSora400, 500, 600, 700

Handwriting (friendly, personal)

Font IDNameWeights
caveatCaveat400, 700
dancing-scriptDancing Script400, 700
pacificoPacifico400

Monospace (code, technical)

Font IDNameWeights
fira-codeFira Code400, 500, 700
jetbrains-monoJetBrains Mono400, 500, 700
Custom fonts: Standard plan users can upload their own fonts (WOFF2, WOFF, TTF, OTF). Custom font IDs use the 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 loading
  • javascript: in url() : no script injection
  • expression() : no IE expression evaluation
  • behavior: : 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

Shell selectors
body { }                        /* Page background (outside the player) */
#slate-player { }               /* The entire player container */
#player-content { }             /* Main content area */

Header

Header selectors
#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

Navigation selectors
#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

Footer selectors
#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

Text selectors
.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

Media selectors
.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

Interactive block selectors
/* 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 selectors
.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 selectors
.divider-line { }               /* Line divider */
.divider-dots::before { }       /* Dotted divider */

Tables

Table selectors
.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 selectors
.hotspot-popover { }            /* Popover container */
.hotspot-popover-header { }     /* Popover header */
.hotspot-popover-body a { }     /* Links inside popovers */
.hotspot-marker.active { }      /* Active hotspot marker */

Knowledge checks

Knowledge check selectors
.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 selectors
.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

Scrollbar & focus selectors
::-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.

Dark theme: inverted color scale
: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.

Midnight Aurora: settings JSON
{
  "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.

Classic Dark: settings JSON
{
  "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:

Theme export JSON schema
{
  "_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) or rgba() format
  • hoverColor can be an empty string (auto-derived) or a valid color
  • outlineThickness: integer 0-4
  • borderRadius: 0-24
  • spacing: 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

  1. Export: Go to Theme page, click the export button to download a .json file
  2. Import: Click import and select a theme JSON file
  3. 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.

1Set the theme settings

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
2Write the Custom CSS: color scale

Open the Custom CSS editor and start with the inverted color scale:

Step 2: Inverted color scale + accent colors
/* 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;
}
3Style the shell
Step 3: Body & player background
body {
  background: #0a0f13;
  color: var(--slate-700);
}

#slate-player {
  background: var(--slate-50);
}
4Style header and footer
Step 4: Header & footer
#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); }
5Style the navigation
Step 5: Navigation sidebar
#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; }
6Style content blocks
Step 6: Content block overrides
.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); }
7Export and share

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.active create visual interest
  • Glassmorphism via backdrop-filter: blur(12px) + semi-transparent rgba() 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-primary and .slate-button.button-primary for full coverage
  • Scrollbar styling makes a big difference in dark themes. Match it to your color scheme