CSS is Awesome

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.

Each shipped theme emits :root, :root[data-theme="name"], so a single file dropped in as your theme.css applies with no markup change. The library's own defaults are emitted under :where(:root) at zero specificity, so any theme — or any token you set from your own :root — outranks them regardless of load order.

/* 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.

Each swatch shows the token name plus the resolved CSS value. Open the ThemePicker (the floating disc, lower-right) and pick a different theme — the chips and values rewrite live, no reload required.

Neutrals
--paper
--paper-raised
--paper-sunk
--paper-glass
--ink
--ink-soft
--ink-faint
--graphite
--muted
--hair
--hair-soft
--guide
--guide-soft
Brand
--brand-primary
--brand-primary-hover
--ai
--ai-ink
--ai-wash
--shu
--shu-wash
--ochre
--ochre-wash
Action (semantic primary)
--action-primary-default
--action-primary-hover
--action-primary-active
--action-primary-wash
Status
--success-default
--success-subtle
--success-text
--warning-default
--warning-subtle
--warning-text
--error-default
--error-subtle
--error-text
--info-default
--info-subtle
--info-text
Surfaces
--surface-default
--surface-raised
--surface-sunk
--surface-subtle
--surface-muted
--surface-emphasis
--surface-glass
/* 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.

--font-displayDraft & revise.
--font-serifReading text sits in a warm serif, patient and honest.
--font-sans / --font-primaryThe default body font — crisp, quiet, does its job.
--font-scripthandwritten marginalia
--font-monoconst paper = '#F7F3EA';
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. Each line below renders at the size resolved from the active theme.

--font-size-base (body)
The quick brown fox jumps over the lazy dog
h4 — 1.25× base
The quick brown fox jumps over the lazy dog
h3 — 1.5× base
The quick brown fox jumps over the lazy dog
h2 — 2× base
The quick brown fox jumps over the lazy dog
h1 — 2.5× base
The quick brown fox jumps over the lazy dog
h1 { font-size: calc(var(--font-size-base) * 2.5); }
h2 { font-size: calc(var(--font-size-base) * 2); }
h3 { font-size: calc(var(--font-size-base) * 1.5); }
p  { font-size: var(--font-size-base); }

Spacing

Spacing is a theme concern like every other token. The numbered scale --space-0 through --space-9 is the source of truth and is contract-required — a theme declares it, and re-proportions the whole page. Values are declared in rem so they honor the user's root font size.

--space-0
--space-1
--space-2
--space-3
--space-4
--space-5
--space-6
--space-7
--space-8
--space-9
.stack > * + * {
  margin-top: var(--space-4);
}
.card { padding: var(--space-5); }

The t-shirt aliases

The six t-shirt names are optional. The library emits them as var() references into the numbered scale — --space-md: var(--space-4) — so they follow whatever the theme sets, automatically. A theme never has to declare them.

They used to be emitted as independent literals: components call space(4)var(--space-4), but a theme could only set the t-shirt names, so the token the theme set and the token the component read were different variables. That is why every shipped theme used to have byte-identical spacing no matter what it declared.

--space-2xs
--space-xs
--space-sm
--space-md
--space-lg
--space-xl
/* emitted by the library, not by the theme */
--space-xs: var(--space-1);
--space-sm: var(--space-2);
--space-md: var(--space-4);
--space-lg: var(--space-5);
--space-xl: var(--space-6);

Radii

Two families ship together: the short --r-sm/md/lg tokens themes use natively, and the longer --radius-sm/md/lg/xl/full aliases the library mixins consume. Both render below at their current theme value — flat in a brutalist mood, generously rounded in a soft one.

--radius-sm
--radius-md
--radius-lg
--radius-xl
--radius-full
--r-sm
--r-md
--r-lg
.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.

--shadow-sm
--shadow-md
--shadow-lg
--shadow-xl
--shadow-2xl
.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.

--duration-fast180ms
--duration-normal240ms
--duration-slow380ms
--easecubic-bezier(.33,.66,.33,1)
.button {
  transition: background var(--duration-fast) var(--ease);
}

The contract

Every theme file must declare the full set of 127 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.

A further 36 optional tokens (163 in total) are recognised but not demanded. These are the per-component radius overrides (--btn-radius, --card-radius, --input-radius, --modal-radius, --badge-radius, --tag-radius), the named shadow slots, the logo hooks, and the t-shirt spacing aliases. Leave them out and each one cascades from the generic scale it belongs to.

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.

Theme