Design system
The colors, type, space and motion every component is built from.
Color
Scales
Every color comes from a Radix Colors scale of 12 steps. Text uses the solid steps; fills and edges use the translucent alpha versions, so they read the same on any surface.
Roles
Components ask for a role, never a step, so every role flips with the theme. Each status has a base for icons and short labels, FillSubtle and Fill tints, and an Edge: dangerFillSubtle, successEdge and so on.
| Token | For |
|---|---|
textPrimary | Content |
textSecondary | Supporting copy |
textMuted | Labels and hints |
fillSubtle | Hover |
fill | Selected |
fillStrong | Pressed |
edge | Borders and dividers |
edgeStrong | Edges that have to hold their own |
accent | The one solid fill, with onAccent text |
background | The page |
danger | Errors and destructive actions |
success | Completed actions |
warning | Something needs attention |
info | Neutral news that isn't an error |
Keep body text neutral on a status tint. The base color marks the icon or label that carries the meaning.
Typography
Components set size and weight and inherit the font family. This site sets Inter; use it or your own. Every size shares one line height, lineHeights.row, so a row of mixed sizes still centers in a control.
Inter
ABCDEFGHIJKLMNOPQRSTUVWXYZ
abcdefghijklmnopqrstuvwxyz
0123456789 !?&@#%
| Token | Value | For |
|---|---|---|
fontSizes.sm | 14px | Controls and body copy |
fontSizes.xs | 13px | Secondary text |
fontSizes.xxs | 12px | The smallest labels |
fontWeights.regular | 400 | Body |
fontWeights.medium | 500 | Labels and titles |
fontWeights.semibold | 600 | Emphasis; nothing is heavier |
Anything that changes in place (counts, timers, prices, table columns) sets fontVariantNumeric: "tabular-nums", so digits don't shift.
Layout
Space
A 4px grid, with 2px for hairline gaps.
| Token | Value |
|---|---|
xxxs | 2px |
xxs | 4px |
xs | 8px |
sm | 12px |
md | 16px |
lg | 24px |
xl | 48px |
Radius
sm for controls, md for surfaces, full for pills. Nested corners are concentric: md around sm at space.xxs.
Size
Controls take a fixed height, so a button and an input in one row line up.
| Token | Value | For |
|---|---|---|
controlXxs | 20px | Inline controls |
icon | 16px | Icons beside sm text |
iconSm | 14px | Icons beside xs text |
iconXs | 12px | Icons beside xxs text |
hitArea | 40px | The smallest target a pointer gets |
Shadow
| Token | For |
|---|---|
control | Under a bordered control |
popover | Floating surfaces; it draws their edge, so they need no border |
Motion
Under media.reducedMotion, fades stay and movement goes: things that travel land at once.
Easing
Each curve over durations.move. Slow it down to see the shape.
cubic-bezier(0.23, 1, 0.32, 1)300ms × 4 = 1200ms
| Token | For |
|---|---|
out | Entrances and presses |
inOut | Things traveling across the screen |
overshoot | Small elements that should feel alive |
layout | A surface growing or shrinking to fit |
crossfade | One state replacing another in place |
Color changes on hover use the CSS keyword ease. Nothing uses ease-in.
Duration
Interface motion stays under 300ms. A spinner is the exception.
| Token | Value | For |
|---|---|---|
hover | 150ms | Color changes on hover |
press | 160ms | Scale on :active |
popover | 180ms | Small surfaces that open from a trigger |
move | 300ms | Indicators and thumbs that travel |
layout | 300ms | A surface growing or shrinking to fit |
crossfade | 300ms | Cross-fades between states |
spin | 1s | One turn of a spinner |
Press
Pressables scale down on :active, on easings.out over durations.press. The smaller the target, the bigger the give.
| Token | Value | For |
|---|---|---|
icon | scale(0.95) | Icon buttons |
link | scale(0.97) | Buttons |
row | scale(0.99) | Full-width rows |