Foundations
Theme
One set of CSS variables drives every component. Pick a preset or flip dark mode below and the entire page repaints — because nothing here is a fixed colour, it all reads from tokens like var(--pri) and var(--surface). Choices persist across reloads.
npx shadcn@latest add @orbit/themePlayground
Live — this switcher drives the whole docs site, including every swatch below. Every element in the preview is painted from tokens; switch preset or mode and watch it all repaint at once.
Tokens
The vocabulary. Components reference these names, never raw hex. Soft tints and their foregrounds are derived with color-mix against the active surface, so they adapt to light and dark for free. Every swatch below is painted with var(--token) — printed values are the light-mode Blue defaults, shown as documentation only.
Presets
Six brand palettes. Each preset re-points the brand tokens; surfaces, text, and borders stay shared and only shift with the mode. Click one to try it — the whole site follows.
Using tokens
Three rules cover almost everything: reference tokens in styles, switch by setting two attributes, and drive it from the runtime.
Use var(--token) anywhere you would write a colour — or the 1:1 Tailwind utilities (bg-surface, text-text-2, border-border-2, bg-pri-soft). Pair a fill with its on-colour so contrast holds in every theme.
style="background: var(--pri);
color: var(--pri-fg)"
className="bg-surface text-text
border border-border"The preset and mode live on the root element. Set them on <html> and the whole subtree repaints.
<html data-theme="violet" data-mode="dark"> /* presets: blue · violet · emerald amber · rose · slate */
Use the useTheme() hook in components, or the OrbitTheme store anywhere. It persists to localStorage and — with <ThemeScript /> in <head> — applies before first paint.
import { OrbitTheme } from "@/registry";
OrbitTheme.setPreset("emerald");
OrbitTheme.setMode("dark");
OrbitTheme.toggleMode();
OrbitTheme.onChange((t) => {/* react */});Runtime API
ThemeControls is a thin UI over the OrbitTheme store (registry/theme).
| Prop | Type | Default | Description |
|---|---|---|---|
| useTheme() | hook | — | Subscribes to { preset, mode } and exposes setPreset, setMode, toggleMode, presets. |
| OrbitTheme.get() | () => ThemeState | — | Current { preset, mode }. |
| OrbitTheme.set() | (Partial<ThemeState>) => void | — | Set either or both; persists and emits an 'orbit-theme' CustomEvent. |
| OrbitTheme.setPreset() / setMode() / toggleMode() | (id) => void | — | Convenience setters over set(). |
| OrbitTheme.onChange() | (fn) => unsubscribe | — | Subscribe outside React. |
| <ThemeScript /> | component | — | Inline script for <head> that applies the saved theme before first paint. |
Radius, shadow & motion
The non-colour scale from the design handoff. Shadows are mapped to Tailwind utilities (shadow-card, shadow-popover, shadow-modal); the overlay ease is --ease-overlay.
| Prop | Type | Default | Description |
|---|---|---|---|
| radius · controls | 7–10px | — | Buttons, inputs, menu items, small chips. |
| radius · containers | 12–18px | — | Cards, popovers, modals, panels. |
| radius · pills / avatars | 999px / 50% | — | Count pills, status pills, circular avatars. |
| motion · micro | .1–.2s ease | — | Hover, press, and focus transitions. |
| motion · overlays | cubic-bezier(.32,.72,0,1) | — | Popover / modal entrances (--ease-overlay), with a small translateY(4–6px) + scale(.98→1) pop. |
Props
The token reference above is built from two doc primitives in the registry — TokenSwatch, a live 30px swatch painted straight from a CSS custom property so it re-themes with the active preset and mode, and TokenSwatchGrid, an auto-fill grid with a group label.
| Prop | Type | Default | Description |
|---|---|---|---|
| token | string | — | The CSS custom property to paint, e.g. "--pri". Every swatch reads var(token) live, so it follows the active preset and mode. |
| kind | "color" | "soft" | "text" | "border" | "on" | "color" | Swatch treatment — solid fill, tint fill with a square of its paired -soft-fg, an “Ag” specimen in the token colour, a 2px outline, or an “A” over its paired fill. |
| pairToken | string | — | For soft: the paired -soft-fg token (drawn as a small square). For on: the fill token rendered behind the glyph. |
| note | string | — | Short usage note under the token name. |
| value | string | — | Optional literal value printed in mono after the note — documentation text only, never used for styling. |
| …rest | HTMLAttributes<HTMLDivElement> | — | Everything else is forwarded to the card root, including ref and className. |
| Prop | Type | Default | Description |
|---|---|---|---|
| TokenSwatchGrid.label | string | — | Mono uppercase group label above the grid. |
| TokenSwatchGrid.children | ReactNode | — | TokenSwatch cards, laid out in an auto-fill grid (min 196px per card). |
| TokenSwatchGrid …rest | HTMLAttributes<HTMLDivElement> | — | Forwarded to the wrapper div, including className. |
Usage
Pair fills with their on-colours
bg-pri with text-pri-fg, bg-success-soft with text-success-soft-fg — the pairs are tuned to hold contrast in every preset and mode.
Hard-code hex values
A raw #2563EB won’t follow the preset or dark mode. If a spec needs a derived shade, use color-mix(in srgb, var(--token) N%, …).
Use soft pairs for tints
The -soft/-soft-fg tokens are mixed against the active surface, so tinted chips and banners adapt to dark mode for free.
Assume light mode
Check every surface in dark mode and all six presets — anything painted from tokens survives automatically; anything else won’t.
Keyboard & accessibility
Theming is a contrast contract — the pairs do the work.