Themes
Themes are the semantic color layer. Every token describes a role — text, surface, border, icon, a status intent — and maps onto a raw palette value. Components consume only these tokens, never the palette directly, so the whole product can be re-themed (including dark mode) by swapping one file.
How theming is layered
Color is organized in two tiers. Keeping them separate means a new theme only redefines roles — it never touches the palette or any component.
| Tier | File | Contains |
|---|---|---|
| 1 — Global color scheme | color-scheme.css | Raw palette scales (50–950). No roles, no meaning. |
| 2 — Theme (default) | themes/light.css | All semantic --color-* / --viz-* tokens, mapped onto Tier 1. |
| 2 — Theme (dark) | themes/dark.css | Opt-in .dark overrides that redeclare the same --color-* names. |
Tokens are category-first: --color-text* (foreground), --color-bg* (backgrounds & surfaces), --color-border*, --color-icon*, plus four status intents (success / warning / error / info) and --color-primary. The light theme is the single source of truth; a small set of base value-holders (--background, --primary, …) sit behind the aliases so light/dark can swap one value — don't consume those directly.
Darken & lighten on the fly
The palette is a fixed 50–950 ramp. When you need a shade that isn't on it — a hover one notch darker, a tint one notch lighter — derive it with CSS relative colors in OKLCH, where lightness is perceptually uniform. The shared step helpers in color-scheme.css name the conventional amounts.
/* one notch darker for a hover state */
.button:hover {
background: oklch(
from var(--color-primary)
calc(l - var(--shade-darken-2)) c h
);
}
/* a subtle tint of any role color */
.badge {
background: oklch(
from var(--color-info)
calc(l + var(--shade-lighten-3)) c h
);
}Step helpers: --shade-darken-1/-2/-3 and --shade-lighten-1/-2/-3 (lightness deltas, subtle → strong). Relative color syntax requires Chrome/Edge 119+, Safari 16.4+, Firefox 128+.
Using semantic tokens
.card {
background: var(--color-bg-surface);
color: var(--color-text);
border: 1px solid var(--color-border);
}
.card__meta {
color: var(--color-text-secondary);
}
.alert--error {
background: var(--color-bg-error);
color: var(--color-text-error);
border: 1px solid var(--color-border-error);
}Text
Foreground colors for text and labels. Default text plus a de-emphasis scale, an on-fill color for text over strong/colored backgrounds, links, and per-status text.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-text | foreground | Default body & heading text | |
| --color-text-secondary | volcanic-ash-600 | Secondary text, captions, metadata | |
| --color-text-muted | volcanic-ash-500 | Muted / placeholder text | |
| --color-text-disabled | volcanic-ash-400 | Disabled text | |
| --color-text-on-fill | #FFFFFF | Text on strong / colored status fills | |
| --color-text-inverse | volcanic-ash-50 | Text on a dark / inverse surface (light or dark theme) | |
| --color-text-inverse-secondary | volcanic-ash-300 | Secondary text on inverse surface | |
| --color-text-inverse-muted | volcanic-ash-400 | Muted text on inverse surface | |
| --color-text-link | glacial-lake-600 | Links | |
| --color-text-link-hover | glacial-lake-700 | Link hover | |
| --color-text-success | moss-canopy-700 | Success text/icon on light bg | |
| --color-text-warning | solar-flare-700 | Warning text/icon on light bg | |
| --color-text-error | thermal-bloom-700 | Error text/icon on light bg | |
| --color-text-info | ion-drive-700 | Info text/icon on light bg |
Background & surface
Backgrounds for the page and components, with hover / active / selected states, a muted surface, inverse surfaces for primary actions and nav, and soft per-status surfaces.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-bg | background | Page background | |
| --color-bg-surface | #FFFFFF | Component / card surface | |
| --color-bg-surface-elevated | #FFFFFF | Raised surface | |
| --color-bg-surface-secondary | volcanic-ash-50 | Subtle / secondary panel fill | |
| --color-bg-surface-hover | volcanic-ash-100 | Hover background on surfaces | |
| --color-bg-surface-active | volcanic-ash-150 | Pressed background | |
| --color-bg-surface-selected | volcanic-ash-100 | Selected item background | |
| --color-bg-muted | muted | Muted near-white surface | |
| --color-bg-inverse | volcanic-ash-950 | Inverse surface (primary buttons) | |
| --color-bg-inverse-hover | volcanic-ash-900 | Inverse hover | |
| --color-bg-inverse-active | volcanic-ash-950 | Inverse active | |
| --color-bg-nav | volcanic-ash-950 | Main navigation background | |
| --color-bg-success | moss-canopy-50 | Soft success surface | |
| --color-bg-warning | solar-flare-50 | Soft warning surface | |
| --color-bg-error | thermal-bloom-50 | Soft error surface | |
| --color-bg-info | ion-drive-50 | Soft info surface |
Border
Borders and dividers: standard, subtle hairlines, a stronger rule, the focus outline, and per-status borders.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-border | border | Standard borders | |
| --color-border-subtle | volcanic-ash-100 | Subtle dividers / hairlines | |
| --color-border-strong | volcanic-ash-300 | Emphasized rule | |
| --color-border-focus | ring | Focus outline | |
| --color-border-success | moss-canopy-200 | Success border | |
| --color-border-warning | solar-flare-200 | Warning border | |
| --color-border-error | thermal-bloom-200 | Error border | |
| --color-border-info | ion-drive-200 | Info border |
Icon
Neutral icon colors. Status icons reuse the --color-text-{status} tokens; icons on a strong fill use --color-icon-on-fill.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-icon | volcanic-ash-600 | Default icon | |
| --color-icon-secondary | volcanic-ash-500 | De-emphasized icon | |
| --color-icon-disabled | volcanic-ash-400 | Disabled icon | |
| --color-icon-on-fill | #FFFFFF | Icon on strong / colored fill | |
| --color-icon-inverse | volcanic-ash-50 | Icon on a dark / inverse surface | |
| --color-icon-inverse-secondary | volcanic-ash-400 | Secondary icon on inverse surface |
Status & intent
Four intents — success, warning, error, info. Each has a solid vivid fill plus the text/surface/border tokens above. Use the solid for bold fills (dots, badges) and the -on-fill token for text/icons on top of it.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-success | moss-canopy-500 | Solid success fill | |
| --color-success-on-fill | #FFFFFF | Text/icon on success fill | |
| --color-warning | solar-flare-500 | Solid warning fill | |
| --color-warning-on-fill | volcanic-ash-900 | Text/icon on warning fill | |
| --color-error | alpine-rose-500 | Solid error fill | |
| --color-error-on-fill | #FFFFFF | Text/icon on error fill | |
| --color-error-hover | alpine-rose-900 | Error/destructive hover | |
| --color-info | ion-drive-500 | Solid info fill | |
| --color-info-on-fill | volcanic-ash-900 | Text/icon on info fill |
Primary & interactive
Primary action color and its on-fill foreground, plus the secondary/accent aliases.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-primary | primary | Primary action | |
| --color-primary-on-fill | primary-foreground | Text/icon on primary | |
| --color-primary-hover | volcanic-ash-800 | Primary hover | |
| --color-secondary | secondary | Secondary surface | |
| --color-secondary-foreground | secondary-foreground | Text on secondary | |
| --color-accent | accent | Accent surface / hover | |
| --color-accent-foreground | accent-foreground | Text on accent |
Sidebar
Surface and text tokens for the app shell navigation.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-sidebar | sidebar | Sidebar background | |
| --color-sidebar-foreground | sidebar-foreground | Sidebar text | |
| --color-sidebar-accent-foreground | sidebar-accent-foreground | Text on sidebar accent |
Avatar accents
Accent fills for avatars without an image, keyed off the palette.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-avatar | avatar | Default avatar fill | |
| --color-avatar-foreground | avatar-foreground | Avatar initials | |
| --color-avatar-teal | glacial-lake-600 | Teal accent | |
| --color-avatar-green | moss-canopy-700 | Green accent | |
| --color-avatar-purple | nebula-spark-600 | Purple accent | |
| --color-avatar-blue | glacial-lake-700 | Blue accent | |
| --color-avatar-amber | solar-flare-700 | Amber accent | |
| --color-avatar-tan | sienna-glow-300 | Tan accent | |
| --color-avatar-tan-dark | sienna-glow-400 | Tan (dark) accent |
Overlays
Translucent black for scrims, shadows, and subtle fills. Number = opacity percent.
| Token | Maps to | Usage | |
|---|---|---|---|
| --color-overlay-5 | rgba(0,0,0,.05) | Hairline fill | |
| --color-overlay-10 | rgba(0,0,0,.10) | Subtle fill / shadow | |
| --color-overlay-20 | rgba(0,0,0,.20) | Light scrim | |
| --color-overlay-30 | rgba(0,0,0,.30) | Scrim | |
| --color-overlay-50 | rgba(0,0,0,.50) | Modal backdrop | |
| --color-overlay-90 | rgba(0,0,0,.90) | Heavy overlay |
Visualization
Data-viz hues, each with a primary (bold) and secondary (tint) for charts and graphs.
| Token | Maps to | Usage | |
|---|---|---|---|
| --viz-green-primary | moss-canopy-800 | Green series | |
| --viz-green-secondary | moss-canopy-200 | Green tint | |
| --viz-teal-primary | glacial-lake-700 | Teal series | |
| --viz-teal-secondary | glacial-lake-200 | Teal tint | |
| --viz-blue-primary | ion-drive-700 | Blue series | |
| --viz-blue-secondary | ion-drive-200 | Blue tint | |
| --viz-indigo-primary | glacial-lake-900 | Indigo series | |
| --viz-indigo-secondary | glacial-lake-200 | Indigo tint | |
| --viz-purple-primary | nebula-spark-800 | Purple series | |
| --viz-purple-secondary | nebula-spark-200 | Purple tint | |
| --viz-pink-primary | alpine-rose-600 | Pink series | |
| --viz-pink-secondary | alpine-rose-200 | Pink tint | |
| --viz-red-primary | thermal-bloom-600 | Red series | |
| --viz-red-secondary | thermal-bloom-200 | Red tint | |
| --viz-orange-primary | sienna-glow-600 | Orange series | |
| --viz-orange-secondary | sienna-glow-200 | Orange tint | |
| --viz-yellow-primary | solar-flare-600 | Yellow series | |
| --viz-yellow-secondary | solar-flare-200 | Yellow tint | |
| --viz-gray-primary | volcanic-ash-200 | Gray series | |
| --viz-gray-secondary | volcanic-ash-100 | Gray tint |
Notes
- Components consume only --color-* tokens — never the raw palette (--volcanic-ash-* etc.) and never the base value-holders.
- For muted text use --color-text-muted; --color-bg-muted is a near-white surface, not a text color.
- Status icons reuse the --color-text-{status} tokens; there is no separate status-icon family.
- Inverted ≠ dark theme. The --color-text-inverse* / --color-icon-inverse* tokens are for content on a dark/inverse surface (a dark panel, nav, or inverse button) — which happens in light theme too. They pair with --color-bg-inverse and flip automatically in dark theme. Components use the inverted prop, not a theme switch.
- Dark mode is class-based (.dark) and driven entirely by themes/dark.css — components carry no per-element dark overrides.