Design tokens
Every visual decision in the system lives in one file: theme.css. Swap that file and the entire UI reskins — buttons, cards, shadows, type, spacing — without touching a line of component CSS.
How tokens work
Tokens are plain CSS custom properties declared on :root. Components read them with var(--token), so a single stylesheet swap cascades everywhere. No build step is required to consume them — drop the file in, and the browser does the rest.
/* theme.css — the one file you swap */ :root { --paper: #F7F3EA; --ink: #2A241E; --ai: #1F3A5F; --radius-md: 3px; --shadow-md: 0 4px 18px rgba(42,36,30,.08); } /* any consumer — base, component, or your own CSS */ .button { background: var(--paper-raised); color: var(--ink); border-radius: var(--radius-md); box-shadow: var(--shadow-md); }
Palette
Color tokens split into three intents: neutrals (paper & ink), brand (indigo, vermilion, ochre), and status (success / warning / error / info). Every swatch below reads live from the active theme — pick a new theme and the whole grid reskins.
Neutrals
Brand
Status
Surfaces
/* Consume any color token the same way */ .card { background: var(--surface-raised); color: var(--text-primary); border: 1px solid var(--border-default); }
Typography
Four font stacks cover the whole system: a chunky display serif, a reading serif, a handwritten script for accents, and a mono for code. A sans stack anchors the body. Size, weight, and line-height live in their own slots so themes can retune the whole rhythm.
body { font-family: var(--font-primary); font-size: var(--font-size-base); line-height: var(--line-height-normal); } h1 { font-family: var(--font-display); } code { font-family: var(--font-mono); }
Type scale
Themes declare --font-size-base, --line-height-normal, and --font-weight-medium as the canonical hooks. Scale steps are computed from the base so the whole system stays proportional.
Spacing
A six-step t-shirt scale drives every gap, padding, and margin in the system. Values are declared in rem so they honor the user's root font size.
.stack > * + * { margin-top: var(--space-md); } .card { padding: var(--space-lg); }
Radii
Two families ship together: the short --r-* tokens themes use natively, and the longer --radius-* aliases the library mixins consume. They point at the same values.
.button { border-radius: var(--radius-md); } .avatar { border-radius: var(--radius-full); }
Shadows
Five elevation steps, from a hairline edge to a full modal lift. The Sketchbook theme tunes them as ink bleeding through paper; a glass theme would swap them for frosted bloom with the same token names.
.modal { box-shadow: var(--shadow-2xl); } .popover { box-shadow: var(--shadow-lg); }
Transitions
Motion is a theme concern — a paper theme uses a soft ease, a neon theme might snap. Three durations and one easing cover the system.
.button { transition: background var(--duration-fast) var(--ease); }
The contract
Every theme file must declare the full set of 123 required tokens — even if a given theme sets some of them to neutral values (e.g. --blur-md: none;). That's what guarantees the one-file swap stays lossless: no matter which theme you drop in, the base stylesheet and components always find the slots they read.
The authoritative contract lives in two places: CONTRACT.md (human-readable, grouped and typed) and scripts/theme-contract.json (machine-readable, consumed by the validator). Run the validator against any theme file to confirm it covers the contract:
# validate a theme file against the contract node scripts/theme-validator.js public/theme.css
Need a starting point? Download any of the built-in starter themes from /docs/install#download, open it up, and change the values. Every token you'll ever need is already slotted in.