Skip to main content

Theme Variants

3 min readStableBeginner

lib/theme-variants.ts registers 26 named palettes. Each one carries a full set of light values, a full set of dark values, and a remap of Tailwind's blue scale, all emitted as CSS custom properties into an inline <style> in the document head. Picking one is a single environment variable — no code, no CSS overrides.

Quick Start

Leave the default in place and the site renders in violet.

.env.local
Bash
THEME_VARIANT=violet

The variable is read once when the server starts, so restart npm run dev after changing it.

Adding Options

Every name below is a valid value. Names are matched lowercase, so AMBER and amber both land on the same palette.

emerald

Green-leaning OKLCH scale at hue 162, distinct from green and teal.

amber

Own OKLCH scale of 11 stops at hue 72.

blue

Own OKLCH scale at hue 250 — an actual blue.

cyan

Own OKLCH scale.

fuchsia

OKLCH scale shared with pink.

green

Own OKLCH scale at hue 148.

indigo

Own OKLCH scale.

lime

Own OKLCH scale.

mauve

Near-neutral scale with a faint magenta cast.

mist

Near-neutral scale with a faint blue cast.

neutral

Same near-neutral scale as zinc.

olive

Near-neutral scale with a faint yellow-green cast.

orange

Own OKLCH scale.

pink

Own OKLCH scale at hue 345.

purple

Own OKLCH scale at hue 308.

red

Own OKLCH scale at hue 25.

rose

Own OKLCH scale at hue 10.

sky

Own OKLCH scale, and an actual blue.

slate

Same near-neutral scale as zinc.

stone

Warm-leaning near-neutral scale.

taupe

Near-neutral scale with a faint warm brown cast.

teal

Own OKLCH scale.

violet

Default. Own OKLCH scale at hue 296 — the OwnDocs brand hue.

yellow

Own OKLCH scale at hue 95.

zinc

Cool-leaning near-neutral scale.

gray

Same near-neutral scale as zinc.

.env.local
Bash
THEME_VARIANT=slate

Four names resolve to one identical near-neutral scale, which is worth knowing before you spend an afternoon comparing them:

  • gray, neutral, slate, and zinc

Every other name has its own hue. That leaves 23 visually distinct results across the 26 names.

Advanced

Deploy platforms set the same variable, so a Render service picks its palette from render.yaml instead of a local file. An unrecognized name falls back to violet rather than breaking the build.

render.yaml
YAML
envVars:
  - key: THEME_VARIANT
    value: violet

What the chosen variant produces is a single CSS string, formatted here for reading and trimmed to a few properties:

generateThemeCss() output for THEME_VARIANT=blue
CSS
:root {
  --color-bg-primary: #fcfefd;
  --color-accent: oklch(59.6% 0.145 163.225);
  --color-blue-500: oklch(69.6% 0.17 162.48);
}
.dark {
  --color-bg-primary: oklch(26.2% 0.051 172.552);
  --color-accent: oklch(76.5% 0.177 163.223);
}

The generated themes map palette stops to roles the same way every time: light mode takes its surfaces from stops 50 and 100, borders from 200 and 300, and the accent from 600 with 700 on hover. Dark mode takes surfaces from 950, 900, and 800, borders from 800 and 700, and the accent from 400 with 300 on hover. Emerald is the exception — its light and dark values are written out by hand, and only its blue-scale remap comes from a palette.

Two more things ride on the same setting:

  • The dark-mode toggle is separate from the palette. Readers pick light or dark themselves, it's stored in localStorage under docs-theme-mode, and it flips the .dark class on <html>. The variant supplies the colors for both sides of that switch.
  • Open Graph images read the same variant. app/og/route.tsx pulls --color-bg-primary, --color-text-primary, --color-text-tertiary, --color-accent, and --color-border-primary straight off it, so social cards match the site.

Components never reference a palette directly. They use the semantic aliases — surface-primary, surface-secondary, surface-tertiary, content-primary, content-secondary, content-tertiary, edge-primary, edge-secondary, and brand — which resolve to whichever variant is loaded.

Options

THEME_VARIANTenvDefault: violet

One of the 26 palette names. Matched case-insensitively; anything unrecognized falls back to violet.

AVAILABLE_THEMESstring[]

The 26 registered names, exported from lib/theme-variants.ts for anything that needs to list or validate them.

DEFAULT_THEMEstringDefault: violet

The fallback used when THEME_VARIANT is unset or unknown.

getThemeVariant(name?: string) => ThemeVariant

Resolves a name to its variant, lowercasing first and falling back to the default. Used by both the root layout and the Open Graph image route.

generateThemeCss(theme: ThemeVariant) => string

Serializes a variant into the :root { ... } .dark { ... } string injected into the document head.

lightRecord<string, string>

The 16 light-mode custom properties on a variant, including --color-api-code-bg.

darkRecord<string, string>

The dark-mode custom properties, matching light key for key.

blueRecord<string, string>

Eleven --color-blue-* overrides that repoint Tailwind's blue scale at the variant's own stops, so existing blue utility classes follow the theme.

Was this page helpful?