For AI agents: the complete documentation index is available at https://ciderpress.dev/llms.txt, the full documentation bundle is available at https://ciderpress.dev/llms-full.txt, and this page is available as Markdown at https://ciderpress.dev/concepts/themes.md.
Get started →

Themes

Overview

A theme in ciderpress is a brand identity with one or more variants. Each variant is a complete set of design tokens that target either the dark or light aesthetic. ciderpress ships with six built-in themes — Mulled, Honeycrisp, Granny Smith, Amber, Midnight, and Arcade — and dark is the framework's baseline.

The theme block in your config registers which themes the site exposes, controls switcher visibility, and lets you override individual color tokens across every registered theme from one place.

UIJob
☀️ / 🌙 in navSwap between variants within the active theme. Hidden when variantSwitcher: false or the active theme has only one variant
🎨 in site footerSwitch between registered themes. Hidden when themeSwitcher: false or only one theme is registered

Built-in Themes

Mulled

The canonical brand. Deep cider burgundy — the "evening / premium" branch of the apple family. Primary #991b1b with both dark and light variants. The light variant pairs warm cream surfaces (#fbf6f4) with deep burgundy ink for a parchment read; the dark variant uses the same near-black canvas as the other apple themes. The legacy slug 'default' is preserved as an alias and resolves to 'mulled'.

Variants: dark (default) · light

https://docs.example.com

Mulled theme — dark

Honeycrisp

Bright apple-red identity (primary #dc2626) with both dark and light variants — the sun/moon toggle is enabled.

Variants: dark (default) · light

https://docs.example.com

Honeycrisp theme — dark

Granny Smith

Apple-green identity (primary #65a30d) with both dark and light variants — the sun/moon toggle is enabled.

Variants: dark (default) · light

https://docs.example.com

Granny Smith theme — dark

Amber

Warm hearth amber identity (primary #d97706) with both dark and light variants. The light variant pairs parchment surfaces (#fffaf2) with deep-roast brown ink; the dark variant shares the apple-family near-black canvas.

Variants: dark (default) · light

https://docs.example.com

Amber theme — dark

Midnight

Opinionated deep-black blue theme. Background sits at #050505 for a near-pure-black surface. Single-variant (dark only) — the sun/moon toggle is hidden when this theme is active.

Variants: dark

https://docs.example.com

Midnight theme

Arcade

Retro neon-green theme inspired by arcade cabinets and CRT monitors. Single-variant (dark only). Includes custom hover animations: border tracing on cards, CRT scanlines on code blocks, neon pulse on buttons, glow effects on sidebar items.

Variants: dark

https://docs.example.com

Arcade theme

Configuration

The theme block in ciderpress.config.ts registers every theme the site can render and controls the switchers and cross-theme overrides:

import { defineConfig } from 'ciderpress'

export default defineConfig({
  title: 'My Docs',
  theme: {
    themes: ['mulled'],
  },
  pages: [/* ... */],
})

Fields

FieldTypeDefaultPurpose
themesThemeEntry[] (required)Registry of every theme the site can switch between. First entry is the default unless one carries default: true
defaultVariant'light' | 'dark' | 'system''dark'Initial variant to render on first paint
themeSwitcherbooleantrue when themes.length > 1Show the named-theme picker
variantSwitcherbooleantrueShow the light/dark toggle (auto-hidden when the active theme has only one variant)
overridesPartial<ThemeColors>Color tokens applied across every registered theme (see Color Overrides)

Theme entries

Each entry in themes is one of four forms:

  1. Built-in name'mulled', 'honeycrisp', 'grannysmith', 'amber', 'midnight', or 'arcade'.
  2. Built-in reference object{ name: 'mulled', default: true } (lets you mark a non-first entry as the default).
  3. Custom defineTheme(...) envelope{ name, variants, defaultVariant? } produced by defineTheme.
  4. Custom envelope with default marker — any custom envelope plus default: true.

The first entry is treated as the site's default theme unless another entry carries default: true.

theme: {
  themes: [
    { name: 'honeycrisp' },
    { name: 'midnight', default: true },
    { name: 'arcade' },
  ],
}

Forcing a starting variant

Each theme picks its own initial variant when there's no persisted preference. defaultVariant overrides this at the site level:

theme: {
  themes: ['honeycrisp'],
  defaultVariant: 'light',
}

'system' honors the user's OS-level light/dark preference. The user's last variant choice persists in localStorage and overrides this default on subsequent visits.

Hiding the switchers

Set either switcher to false to suppress it from the UI:

theme: {
  themes: ['honeycrisp'],
  variantSwitcher: false,  // single-variant brand experience
}

variantSwitcher is automatically hidden when the active theme has only one variant (midnight, arcade) — CSS keys off data-cp-variants on <html>. themeSwitcher auto-hides when themes has a single entry.

Color Overrides

Override individual color tokens across every registered theme without creating a new custom theme. Overrides apply as inline CSS custom properties and take precedence over each theme's variant defaults.

theme: {
  themes: ['honeycrisp', 'midnight'],
  overrides: {
    brand: '#ff6b6b',
    brandDark: '#cc5555',
  },
}

Use overrides for cross-theme brand consistency — e.g. forcing the same brand red on every theme so the site identity stays recognizable when users switch themes. For per-variant adjustments, prefer a custom theme instead.

Available color tokens

TokenDescription
brandPrimary brand color
brandLightLighter brand variant
brandDarkDarker brand variant
brandSoftSoft/tinted brand for backgrounds
bgMain background
bgAltAlternate background
bgElvElevated surface background
bgSoftSoft background
text1Primary text
text2Secondary text
text3Tertiary/muted text
dividerDivider lines
borderBorder color
homeBgHome page background

Values must be valid CSS hex (#xxx or #xxxxxx) or rgba() values. The schema rejects CSS terminators (;, {, }, </style>) to defend against injection in tenant-supplied themes.

Custom Themes

When the built-in themes plus overrides aren't enough, register a fully custom theme through defineTheme and pass it directly into themes:

import { defineConfig, defineTheme } from 'ciderpress'

const citrus = defineTheme({
  name: 'citrus',
  defaultVariant: 'dark',
  variants: {
    dark: {
      colors: {
        brand: {
          primary: '#ff7a3d',
          hover: '#ff8c54',
          active: '#e65f24',
          fg: '#2a0f06',
          soft: 'rgba(255, 122, 61, 0.14)',
          onBrand: '#2a0f06',
          light: '#ffa56a',
          lighter: '#ffc89a',
        },
        // ... see `CiderpressTokens` for the full token tree
      },
      // ... spacing, radii, fonts, shadows, motion, etc.
    },
    // Add a `light: { ... }` entry to enable the toggle for this theme.
  },
})

export default defineConfig({
  theme: {
    themes: [citrus],
  },
  // ...
})

The theme validates at config time and emits one html[data-cp-theme='{name}'][data-cp-variant='{variant}'] CSS block per variant.

Fonts

A theme declares three font slots under fonts.family. Every font on the page resolves back to one of them — there is no separate stylesheet to patch and no component-level override to write.

fonts: {
  family: {
    sans: "'Courier New', 'Lucida Console', Monaco, monospace",
    mono: "'Courier New', 'Lucida Console', Monaco, monospace",
    display: "'Press Start 2P', monospace", // optional
  },
  // ... weight, size
}
SlotCanonical variableSurfaces
sans--cp-ff-sansBase UI and prose — document body, hero title/tagline/buttons, nav, sidebar, doc text
mono--cp-ff-monoCode blocks, inline code, terminal chrome, OpenAPI fields, eyebrow labels
display--cp-ff-displayDecorative headline surfaces — feature-card titles, brand marks, loader label

display is optional and falls back to sans, so a theme that declares only sans and mono still renders every surface in its own fonts.

sans is the base font, not necessarily a sans-serif face — the built-in themes point it at an Inter stack, and mono is where Geist Mono lives. Pick whatever face the body copy should use.

Compatibility variables

Each theme block also emits these aliases so older rules — and Rspress's own internals — follow the active theme:

VariableResolves to
--cp-font-family-base, --cp-font-family-sansfonts.family.sans
--cp-font-family-monofonts.family.mono
--cp-font-family-pixelfonts.family.display
--rp-font-family-basefonts.family.sans
--rp-font-family-monofonts.family.mono

These are emitted inside the theme's html[data-cp-theme][data-cp-variant] block rather than in a cascade layer. Rspress declares --rp-font-family-* on :root unlayered, and unlayered declarations outrank every @layer regardless of specificity — the theme block is the only place an override wins.

Sharing tokens between variants

Token tree shapes are identical across variants. To share spacing/radii/fonts/etc. between dark and light, use object spread:

const shared = {
  spacing: { ... },
  radii:   { ... },
  fonts:   { ... },
}

defineTheme({
  name: 'citrus',
  variants: {
    dark:  { ...shared, colors: { /* dark palette */ } },
    light: { ...shared, colors: { /* light palette */ } },
  },
})

defineTheme validates each variant against the full token schema, so omitting a leaf surfaces a clear ZodError at config time.

Constraints

ConstraintDetail
Theme name is a slugMust match /^[a-z0-9][a-z0-9-]*$/. Used directly in the html[data-cp-theme='{name}'] selector.
Reserved names rejectedCustom themes cannot reuse a built-in slug (mulled, honeycrisp, grannysmith, amber, midnight, arcade) — those slugs are owned by @ciderpress/theme.
At least one variant requiredvariants must declare dark, light, or both. Empty variants: {} raises a ZodError.
Token tree must be completeEvery leaf in CiderpressTokens is required per variant, except fonts.family.display. defineTheme validates each declared variant through tokensSchema and throws on missing leaves.
fonts.family.display is optionalOmit it and defineTheme fills it from fonts.family.sans before freezing the token tree, so the emitted CSS always carries a --cp-ff-display declaration.
Token values are CSS-safeToken strings reject ;, {, }, and </style> to prevent CSS injection in tenant-supplied themes.
defaultVariant must existWhen provided, the value must point at a declared variants.* entry.
Persisted theme is validatedWhen the theme switcher is enabled, the active theme is persisted in localStorage. On the next visit the value is intersected against the registry — stale entries are cleared and the user falls back to the build-time default.
Persisted variant is validatedThe active variant is persisted in localStorage. On the next visit it's intersected against the active theme's variants — when the user's theme no longer supports that variant, the theme's defaultVariant is applied instead.

Comparing Approaches

ThemeBest ForVariantsDefault variant
MulledPremium / evening reads (canonical)dark, lightdark
HoneycrispBright apple-red, general-purpose docsdark, lightdark
Granny SmithGreen-branded, accessible docsdark, lightdark
AmberWarm hearth / parchment cider aestheticdark, lightdark
MidnightDeveloper tools, dark-first sitesdarkdark
ArcadePlayful branding, creative toolsdarkdark

Design Decisions

  • Themes are brand identities; variants are dark/light. This separation matches Chakra UI's _dark/_light and shadcn/ui's .dark selector — a theme is a brand, and dark/light is a property of the site.
  • Single themes array, single ordering rule. First entry is default unless one is marked. There is no separate name field selecting from a global registry — what's in themes is what the site ships.
  • Separate switchers for theme vs. variant. themeSwitcher and variantSwitcher answer different questions. Coupling them under one toggle made it impossible to ship a brand with only dark mode but still let the user pick between brand A and brand B.
  • Cross-theme overrides. A site that ships three themes with a single shared brand red shouldn't have to re-declare it three times. One block, applied everywhere.
  • CSS custom properties. Overrides are injected as inline CSS variables — debuggable in browser devtools and compatible with any CSS-in-JS approach.
  • Reduced motion support. The Arcade theme's animations respect prefers-reduced-motion, disabling all continuous animations (border tracing, CRT scanlines, neon pulse, glow bar) automatically.

References