Authoring a theme
A theme is a single CSS file declaring the 123 tokens in the contract. The validator guarantees a clean one-file swap.
Overview
- Every theme must declare every token in
scripts/theme-contract.json— currently 123 required tokens. Missing any one of them is a validator failure. - Themes live in
public/themes/<your-theme>/theme.cssas a standalone file, or consolidated into the rootpublic/theme.cssas a[data-theme="<name>"]block. node scripts/theme-validator.jsis the acceptance gate — no theme ships without a passing run.
The token contract
The machine-readable source of truth is scripts/theme-contract.json. It lists every CSS custom property a theme must declare. The validator reads this file — if you add a token to the base library, you add it to the contract, and every theme then has to declare it.
The tokens fall into these categories:
- Surfaces —
--paper,--paper-raised,--paper-sunk,--paper-glass. The page, card and elevated backgrounds. - Ink —
--ink,--ink-soft,--ink-faint,--graphite,--muted. Body text and its soft-to-faint ramp. - Lines —
--guide,--guide-soft,--hair,--hair-soft. Rules, dividers and borders. - Primary —
--ai,--ai-ink,--ai-wash. The main interactive accent (links, primary buttons). - Seal —
--shu,--shu-wash. The emphasis accent (badges, important callouts). - Accent —
--ochre,--ochre-wash. Marginalia, pull-quotes, tertiary accent. - Code —
--code-bg,--code-ink,--code-muted,--code-accent,--code-green,--code-blue. Syntax surface and its semantic tokens. - Type —
--font-display,--font-serif,--font-sans,--font-mono,--font-script,--font-primary, plus size/weight/line-height tokens. - Radius —
--r-sm/md/lg(library short scale) plus--radius-sm/md/lg/xl/full(semantic scale). - Shadow —
--shadow-smthrough--shadow-2xl. - Blur —
--blur-sm/md/lg. Glass and backdrop filters. - Glow —
--glow-sm/md/lg. Focus rings and hover auras. - Motion —
--duration-fast/normal/slow,--ease. - Semantic aliases —
--surface-default,--text-primary,--border-default,--action-primary-*,--interactive-hover,--background-*, etc. These bridge library names to the native palette. - Feedback / status —
--success-*,--warning-*,--error-*,--info-*,--feedback-*. Every status colour has adefault,subtleandtextvariant. - Library scales —
--space-2xsthrough--space-xl, plus the--z-*layering scale (--z-dropdown,--z-modal, etc.).
See /docs/tokens for the full gallery with live swatches and current values for each shipped theme.
File structure
A theme file is a font @import (optional) plus a single :root block that sets every required token. Preserve the commented section headers — they make the file scannable and keep the contract visually grouped.
/* ============================================================ THEME — My Brand ============================================================ */ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300..700&display=swap'); :root { /* Surfaces */ --paper: #FFFFFF; --paper-raised: #F7F7F5; --paper-sunk: #EEEEEA; --paper-glass: rgba(255,255,255,0.80); /* Ink */ --ink: #0A0A0A; --ink-soft: #4A4A4A; --ink-faint: #8A8A8A; /* Lines, Primary, Seal, Accent, Code, Type, */ /* Radius, Shadow, Blur, Glow, Motion... */ /* Semantic aliases — bridge to library names */ --surface-default: var(--paper); --surface-raised: var(--paper-raised); --text-primary: var(--ink); --border-default: var(--hair-soft); }
If you are contributing your theme to the shipped consolidated file instead, the body stays identical — wrap it in a [data-theme="<name>"] selector:
[data-theme="my-brand"] { /* same 123 token declarations */ --paper: #FFFFFF; /* ... */ }
Step by step
- Copy
public/themes/press/theme.css(or any shipped theme) as a starting point. Press is a good editorial baseline; Cupertino is a good rounded/soft baseline; Terminal is a good dark-mode baseline. - Rename the file path to
public/themes/<your-theme>/theme.css. - Change every token value — leave every token name. Work category by category: start with surfaces (
--paper,--paper-raised,--paper-sunk), then ink (--ink,--ink-soft,--ink-faint), then the rest of the palette, then type, then radii and shadows. - Keep the semantic aliases intact:
--surface-default: var(--paper),--text-primary: var(--ink), etc. These bridge library names to your native palette. Change the right-hand side only if your mood genuinely requires a different mapping (e.g. you want--surface-defaultto resolve to--paper-raised). - Run the validator against your file:It prints every missing token. Fix them and re-run until the output reads
$ node scripts/theme-validator.js public/themes/<your-theme>/theme.cssOK. - Once the single file validates, register the theme in the picker. Add an entry (id + label) to the
THEMESarray insrc/components/ThemePicker/ThemePicker.tsx, and add the id to theVALID_THEMESset insrc/app/layout.tsx. - If you are contributing upstream, also add a
[data-theme="<name>"]block to the consolidatedpublic/theme.csswith the same body. The per-theme file is kept for standalone deploys; the consolidated file powers the docs site picker.
The validator
scripts/theme-validator.js is zero-dependency Node. It auto-detects per-file vs consolidated input and reports missing tokens with a non-zero exit code on failure.
node scripts/theme-validator.js public/themes/my-theme/theme.css— validate one standalone file. You can pass multiple paths.node scripts/theme-validator.js --all— discover and validate every shipped theme (the consolidatedpublic/theme.cssplus everypublic/themes/*/theme.css).node scripts/theme-validator.js --help— print usage.
Exit codes:
0— every validated file / block declares every required token.1— one or more files or blocks are missing tokens.2— usage error (file not found, bad args, bad contract).
Wire npm run validate-themes into your pre-commit or CI step and authorship becomes a closed loop: if it passes, it ships.
Design guidance
- Keep
--paperand--inkcontrasting — WCAG AA minimum (4.5:1) for body text, 3:1 for large text. Everything else is built on top of this pair. --aiis the primary accent; make it visually distinct from--shu(emphasis) and--ochre(marginalia). When all three appear on the same page, the reader should immediately know which is the link, which is the badge and which is the pull-quote.- If your theme is dark-mode,
--paperis still your dark surface and--inkis still your light text — the semantic names stay, the values swap. Do not rename tokens. - Status colours (
--success-default,--error-default,--warning-default,--info-default) should hit WCAG AA against--paper. Their-textvariants are for foreground use on-subtlewashes. - Radii, shadows and motion durations define your theme's "voice" as much as colour does. Editorial themes have tight radii (2–4px), near-flat shadows, fast easing. Apple-flavoured themes have generous radii (10–14px), layered shadows, slower easing. Pick a voice and keep it consistent.
Testing
- Run the validator —
node scripts/theme-validator.json your file, then--allto confirm you haven't broken any shipped theme. - Swap the attribute on
<html data-theme="<name>">manually in DevTools and click through every page of the docs site. Every component should re-skin; nothing hard-coded should peek through. If you see a stray colour, the offender is the base library, not the theme — file a bug. - Check contrast for each colour pair with a tool like
a11y.digitala11y.com: ink on paper, link on paper, every status-defaulton paper, every status-texton its matching-subtle.
Shipping
For a standalone deploy, drop your theme file in your own public/themes/<your-theme>/theme.css and link it from your HTML before cia.css. No registration required — the base library reads tokens from whatever you put on :root.
To contribute your theme upstream, open a PR using the Theme Submission template at .github/ISSUE_TEMPLATE/theme_submission.yml. See the full checklist in CONTRIBUTING-THEMES.md at the repo root — it covers validator output, screenshots per page, contrast notes and the picker registration diff.