Installation
Theme
Mode

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/theme

Playground

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.

Theme
Mode
Buttons
PrimarySecondarySoftDanger
Chips
PrimaryAccentSuccessWarningDanger
Field
Focused input
Surface & content
SC
Ship onboarding v2
Due Jun 30 · Sam Chen
In progress
Progress62%
Saved — all changes synced.

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.

Surface
--bg
App canvas · #FAFAFA
--surface
Cards & panels · #FFFFFF
--surface-2
Subtle fill · #F6F6F7
--surface-3
Stronger fill · #F1F1F3
--overlay
Scrim · rgba(16,24,40,.32)
Text
--text
Headings · #18181B
--text-2
Body · #52525B
--text-3
Muted / meta · #A1A1AA
Ink ramp
--ink
Icons & emphasis · #3F3F46
--ink-soft
Secondary ink · #71717A
--text-faint
Faintest hint · #B4B4BC
Border
--border
Hairline · #ECECEF
--border-2
Divider / edge · #E0E0E4
--line
Strong line · #D4D4D8
Brand
--pri
Primary action · #2563EB
--pri-hover
Primary hover · #1D4ED8
--pri-press
Primary pressed · #1E40AF
--pri-fg
Text on primary · #FFFFFF
--pri-soft
Primary tint · color-mix 13%
--pri-soft-fg
On tint · color-mix 70%
--sec
Secondary · #475569
--sec-fg
Text on secondary · #FFFFFF
--sec-soft
Secondary tint · color-mix 13%
--sec-soft-fg
On tint · color-mix 70%
--acc
Accent · #0D9488
--acc-fg
Text on accent · #FFFFFF
--acc-soft
Accent tint · color-mix 14%
--acc-soft-fg
On tint · color-mix 70%
Semantic
--success
Success · #16A34A
--success-soft
Success tint · color-mix 14%
--success-soft-fg
On tint · color-mix 68%
--warning
Warning · #D97706
--warning-soft
Warning tint · color-mix 16%
--warning-soft-fg
On tint · color-mix 70%
--danger
Danger · #DC2626
--danger-soft
Danger tint · color-mix 13%
--danger-soft-fg
On tint · color-mix 70%
--info
Informational (aliases --pri) · var(--pri)
Ring
--ring
Focus ring · pri 42% mix
--danger-ring
Destructive focus ring · danger 40% mix

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.

1Reference tokens in styles

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"
2Switch by setting two attributes

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            */
3Drive it from the runtime

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).

PropTypeDefaultDescription
useTheme()hookSubscribes to { preset, mode } and exposes setPreset, setMode, toggleMode, presets.
OrbitTheme.get()() => ThemeStateCurrent { preset, mode }.
OrbitTheme.set()(Partial<ThemeState>) => voidSet either or both; persists and emits an 'orbit-theme' CustomEvent.
OrbitTheme.setPreset() / setMode() / toggleMode()(id) => voidConvenience setters over set().
OrbitTheme.onChange()(fn) => unsubscribeSubscribe outside React.
<ThemeScript />componentInline 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.

Shadow scale
shadow-card
0 1px 2px rgba(16,24,40,.04)
shadow-popover
0 16px 40px -12px rgba(16,24,40,.24), 0 4px 12px -4px rgba(16,24,40,.1)
shadow-modal
0 24px 60px -16px rgba(16,24,40,.4)
PropTypeDefaultDescription
radius · controls7–10pxButtons, inputs, menu items, small chips.
radius · containers12–18pxCards, popovers, modals, panels.
radius · pills / avatars999px / 50%Count pills, status pills, circular avatars.
motion · micro.1–.2s easeHover, press, and focus transitions.
motion · overlayscubic-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.

Brand
--pri
Primary action
--pri-soft
Primary tint
--pri-fg
Text on primary
PropTypeDefaultDescription
tokenstringThe 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.
pairTokenstringFor soft: the paired -soft-fg token (drawn as a small square). For on: the fill token rendered behind the glyph.
notestringShort usage note under the token name.
valuestringOptional literal value printed in mono after the note — documentation text only, never used for styling.
…restHTMLAttributes<HTMLDivElement>Everything else is forwarded to the card root, including ref and className.
PropTypeDefaultDescription
TokenSwatchGrid.labelstringMono uppercase group label above the grid.
TokenSwatchGrid.childrenReactNodeTokenSwatch cards, laid out in an auto-fill grid (min 196px per card).
TokenSwatchGrid …restHTMLAttributes<HTMLDivElement>Forwarded to the wrapper div, including className.

Usage

Do

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.

Don’t

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%, …).

Do

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.

Don’t

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.

pairsEvery fill token ships with an on-colour (--pri-fg, --success-soft-fg…) so contrast holds in all 6 presets, light and dark.
focusThe focus ring is a token: 0 0 0 3px var(--ring) — a 42% mix of the active primary, so it stays visible on every preset.
motionA global prefers-reduced-motion guard collapses all animation and transition durations to ~0.
persistPreset and mode persist to localStorage and apply before first paint via <ThemeScript /> — no flash, no reset per page.