One system for every web app.
Tokens first, components second. Everything below is built from the same layered CSS
tokens, works in light and dark, and re-skins from a single theme.css.
Start every new project here.
Colour
Neutral by default so any brand can sit on top. Components read only these semantic tokens — never raw values.
Surfaces & borders
Page canvas
Cards, panels
Inputs, active rows
Hover state
Default outline
Hover / focus outline
Dividers
Text & accent
Headings, body
Supporting copy
Labels, meta
Decorative / disabled
Primary action
Text on accent
Hover wash
Keyboard focus
Status — used sparingly (dots, chips, small icons)
Positive / active
Pending / caution
Error / destructive
Neutral highlight
Categorical
Each status colour also yields --*-subtle and --*-border tints for fills and outlines, derived with color-mix().
Typography
Geist for interface text, JetBrains Mono for eyebrows, numbers, code and metadata. Both are self-hosted variable fonts.
Caption · 10
Meta · 11
Small · 12
Base · 13
Body · 14
Lead · 16
Title · 18
H3 · 20
H2 · 24
H1 · 32
Display · 40
Mono · uppercase · tracked
Spacing
Every gap and padding is a multiple of --space-unit (4px). Change the unit to make the whole app denser or roomier.
Radius & shadow
Semantic aliases decide where each radius is used: controls, cards, overlays, badges.
Radius scale
Elevation
Motion
Short and soft. Hover a track to preview the duration. All motion collapses under prefers-reduced-motion.
--duration-instant · .1s
--duration-fast · .15s
--duration-base · .2s
--duration-slow · .3s
Layout
A fixed-chrome shell: topbar, collapsible navigation, scrolling content, optional inspector, slim footer. On mobile the panels become drawers behind a bottom tab bar.
Tuning
Three global knobs re-scale the entire system at once. Try them — every component on this page responds.
--space-unit: 4px--radius-scale: 1--type-scale: 1Buttons
One primary action per view. Use ghost for toolbars and icon buttons for dense chrome.
Variants
Sizes, icons & states
Forms
Inputs sit on the raised surface with a soft focus halo. Always pair with a visible label.
Shown to teammates.
Enter a valid email address.
Selection
Badges & avatars
Chips carry status and counts. Colour is never the only signal — pair it with a label or dot.
Chips
Avatars
Data display
Cards group related content; tables carry dense records with mono numerals.
Body content sits on the surface token.
Table
| Item | Owner | Status | Value |
|---|---|---|---|
| Item #0041 | Farhan M. | Active | £48,200 |
| Item #0042 | Aisha K. | Pending | £12,950 |
| Item #0043 | Marcus R. | Error | £3,400 |
Feedback
Tell people what happened, what is loading, and what to do when there is nothing to show.
Progress, spinner & skeleton
Empty state & toast
Create your first item to see it here.
Overlays
Dialogs use the native <dialog> element — Escape and focus trapping come free.
Theming a new project
Copy css/ and fonts/ into the new project, then override only what the brand needs in css/theme.css — it loads last and un-layered, so it always wins.
:root { --radius-scale: .75; --space-unit: 3.5px; }
[data-theme="dark"] { --accent: #7dd3fc; --on-accent: #0b1220; }
[data-theme="light"] { --accent: #0369a1; --on-accent: #ffffff; }
Full guide in THEMING.md. Rules: components use semantic tokens only; no raw colours outside the theme files; light and dark ship together from day one.