Tokens
166 tokens, one source
Defined in W3C Design Tokens JSON under packages/ui/tokens. The build emits CSS custom properties (the names below with dots as hyphens, e.g. --color-text-accent), TypeScript, a Figma variables file, and the contrast report. Colour swatches below are rendered live in each theme.
Type roles
Fifteen roles replace eighteen sizes
type.display-xl · clamp(42px, 6.8vw, 84px) / 1.1 · 700 · -0.03em
Find real edge.
The landing hero.
type.display-lg · clamp(32px, 4.6vw, 52px) / 1.1 · 700 · -0.03em
Find real edge.
Page titles.
type.display-md · clamp(28px, 4.5vw, 40px) / 1.1 · 700 · -0.02em
Find real edge.
Section titles.
type.display-sm · clamp(22px, 3vw, 30px) / 1.1 · 700 · -0.02em
Find real edge.
Sub-section titles.
type.heading · 20px / 1.4 · 700 · -0.02em
Every signal is marked at close against a fixed source.
Card and panel headings.
type.body-lg · 16px / 1.6 · 400 · 0em
Every signal is marked at close against a fixed source.
Lead paragraphs.
type.body · 15px / 1.7 · 400 · 0em
Every signal is marked at close against a fixed source.
Running text.
type.body-sm · 13px / 1.6 · 400 · 0em
Every signal is marked at close against a fixed source.
Dense UI text, table cells.
type.label · 11px / 1.4 · 500 · 0.08em
Every signal is marked at close against a fixed source.
Form labels, table headers, stat captions.
type.eyebrow · 11px / 1.4 · 500 · 0.12em
Every signal is marked at close against a fixed source.
The mono line above a section title.
type.caption · 11px / 1.6 · 400 · 0.08em
Every signal is marked at close against a fixed source.
Notes under a control.
type.code · 13px / 1.6 · 400 · 0em
skynet verify --provider 299M…rpDh
Addresses, signatures, code.
type.data · 14px / 1.4 · 500 · 0em
+0.41 n=127
Numbers in tables and stats. Mono so columns align.
type.control · 16px / 1.4 · 400 · 0em
Every signal is marked at close against a fixed source.
Text typed into a field. 16px so iOS does not zoom.
type.button · 14px / 1.4 · 600 · 0em
Button labels.
Semantic colour
Roles, themed
Each swatch shows the same role in dark and light. Tokens marked with a requirement are checked against it on every build; see Accessibility.
| Token | Value (dark / light) | Description |
|---|---|---|
color.surface.page→ color.neutral.950 | #0d0d0d#f5f5f5 | The page ground. |
color.surface.raised→ color.neutral.900 | #1a1a1a#ffffff | Cards, inputs, table rows: one step above the page. |
color.surface.sunken→ color.neutral.850 | #252525#e8e8e8 | Wells, code blocks, hover fills. |
color.surface.overlay→ color.neutral.900 | #1a1a1a#ffffff | Menus and popovers that float over content. |
color.surface.scrim | #000000a6#00000066 | Behind a modal dialog. Dims the page without hiding it. |
color.text.primary→ color.neutral.150 | #e0e0e0#1a1a1a | Headings and body. |
color.text.secondary→ color.neutral.300 | #c0c0c0#333333 | |
color.text.muted→ color.neutral.400 | #a0a0a0#666666 | Captions, labels, supporting copy. Still body-text contrast: muted is a tone, not an excuse. |
color.text.accent→ color.green.300 | #16ff9a#16ff9a | Accent used as text: links, the italic word in a display heading. |
color.text.link→ color.green.300 | #16ff9a#16ff9a | |
color.text.on-accent→ color.neutral.1000 | #000000#000000 | Text on the accent fill, e.g. the primary button label. |
color.text.disabled→ color.neutral.600 | #666666#a0a0a0 | Disabled text is exempt from contrast minimums (WCAG 1.4.3); never the only cue. |
color.accent.fill→ color.green.300 | #16ff9a#16ff9a | The brand fill: the same green in both themes. On light it sits near 1.2:1 against the page, so the boundary of anything filled with it is drawn by accent.edge, not by the fill. |
color.accent.fill-hover→ color.green.400 | #12cc7a#12cc7a | |
color.accent.edge→ color.green.300 | #16ff9a#007a20 | The boundary of a filled accent control. Dark: the fill's own green, invisible. Light: a darker green so the edge clears 3:1 against the page (WCAG 1.4.11). |
color.border.subtle | #4a4a4a8c#d0d0d08c | Decorative hairlines that divide sections. Exempt from 1.4.11 because they convey nothing on their own. |
color.border.default→ color.neutral.700 | #4a4a4a#d0d0d0 | Card and table outlines. Decorative. |
color.border.control→ color.neutral.500 | #767676#8a8a8a | The boundary of anything you can type into or toggle. 3:1 per WCAG 1.4.11. |
color.border.focus→ color.green.300 | #16ff9a#16ff9a | The focus ring. 3:1 against whatever it sits on. |
color.status.win→ color.green.300 | #16ff9a#16ff9a | |
color.status.loss→ color.red.400 | #ff4444#cc0000 | |
color.status.pending→ color.amber.400 | #ffa500#a85a00 | |
color.decor.pink→ color.pink.400 | #ff6b9d#e85a8a | Second colour on divider segments only. Never text. |
Semantic roles, theme-independent
| Token | Value | Description |
|---|---|---|
layout.gutter→ space.500 | 20px | Page side padding. |
layout.section→ space.1600 | 64px | Vertical gap between page sections. |
layout.stack-sm→ space.200 | 8px | |
layout.stack-md→ space.400 | 16px | |
layout.stack-lg→ space.800 | 32px | |
layout.measure→ size.measure | 680px | |
layout.container→ size.container | 1200px | |
layout.page-top→ space.2000 | 80px | Space above a page's title, below the site header. |
layout.page-gap→ space.2500 | 100px | Space between a page header and the first section. |
motion.hover→ duration.fast | 100ms | Colour and border changes on hover and focus. |
motion.reveal→ duration.base | 200ms | Things appearing: menus, status messages. |
motion.ease→ easing.standard | [0.2,0,0,1] | |
motion.exit→ duration.fast | 100ms | Things leaving. Shorter than reveal: attention is already moving on. |
motion.press→ duration.fast | 100ms | Press feedback on buttons: the scale down and back. |
focus.ring-width→ border-width.thick | 2px | |
focus.ring-offset→ space.50 | 2px | |
shadow.overlay→ shadow.overlay-dark | {"color":"#000000a6","offsetX":"0px","offsetY":"8px","blur":"24px","spread":"0px"} | |
shadow.raised→ shadow.raised-dark | {"color":"#ffffff1f","offsetX":"0px","offsetY":"0px","blur":"0px","spread":"1px"} |
Component tokens
Owned by one component each, and allowed to reference semantic tokens only.
| Token | Value | Description |
|---|---|---|
button.height→ size.control-md | 40px | |
button.height-sm→ size.control-sm | 32px | |
button.radius→ radius.md | 6px | |
button.padding-x→ space.500 | 20px | |
field.height→ size.control-md | 40px | |
field.radius→ radius.md | 6px | |
field.padding-x→ space.300 | 12px | |
field.bg→ color.surface.page | #0d0d0d#f5f5f5 | |
field.border→ color.border.control | #767676#8a8a8a | |
table.cell-padding-x→ space.400 | 16px | |
table.cell-padding-y→ space.300 | 12px | |
table.row-border→ color.border.subtle | #4a4a4a8c#d0d0d08c | |
badge.radius→ radius.sm | 2px | |
badge.padding-x→ space.150 | 6px | |
panel.radius→ radius.md | 6px | |
panel.padding→ space.600 | 24px | |
panel.bg→ color.surface.raised | #1a1a1a#ffffff | |
panel.border→ color.border.default | #4a4a4a#d0d0d0 |
Primitives
Raw values. Never referenced by a component; the test suite checks. Groups: color, space, font, radius, border-width, duration, easing, shadow, size.
| Token | Value | Description |
|---|---|---|
color.neutral.0 | #ffffff#ffffff | |
color.neutral.50 | #f5f5f5#f5f5f5 | |
color.neutral.100 | #e8e8e8#e8e8e8 | |
color.neutral.150 | #e0e0e0#e0e0e0 | |
color.neutral.200 | #d0d0d0#d0d0d0 | |
color.neutral.300 | #c0c0c0#c0c0c0 | |
color.neutral.400 | #a0a0a0#a0a0a0 | |
color.neutral.450 | #8a8a8a#8a8a8a | |
color.neutral.500 | #767676#767676 | |
color.neutral.600 | #666666#666666 | |
color.neutral.700 | #4a4a4a#4a4a4a | |
color.neutral.800 | #333333#333333 | |
color.neutral.850 | #252525#252525 | |
color.neutral.900 | #1a1a1a#1a1a1a | |
color.neutral.950 | #0d0d0d#0d0d0d | |
color.neutral.1000 | #000000#000000 | |
color.green.300 | #16ff9a#16ff9a | The brand accent on dark. |
color.green.400 | #12cc7a#12cc7a | |
color.green.600 | #00aa2e#00aa2e | The original light-theme accent. 2.8:1 on the light page, so it is no longer used for text. |
color.green.700 | #008824#008824 | |
color.green.800 | #007a20#007a20 | Darkened until it clears 4.5:1 on the light page and carries white text. |
color.red.400 | #ff4444#ff4444 | |
color.red.700 | #cc0000#cc0000 | |
color.amber.400 | #ffa500#ffa500 | |
color.amber.700 | #a85a00#a85a00 | The original #ff8800 is 2.4:1 on the light page. |
color.pink.400 | #ff6b9d#ff6b9d | |
color.pink.600 | #e85a8a#e85a8a | |
space.0 | 0px | |
space.25 | 1px | |
space.50 | 2px | |
space.100 | 4px | |
space.150 | 6px | |
space.200 | 8px | |
space.300 | 12px | |
space.400 | 16px | |
space.500 | 20px | |
space.600 | 24px | |
space.800 | 32px | |
space.1000 | 40px | |
space.1600 | 64px | |
space.2000 | 80px | |
space.2500 | 100px | |
font.family.sans | ["var(--font-sans)","-apple-system","BlinkMacSystemFont","Segoe UI","Roboto","sans-serif"] | Inter, self-hosted by the app through next/font. |
font.family.mono | ["var(--font-mono)","ui-monospace","SFMono-Regular","Menlo","monospace"] | JetBrains Mono. |
font.size.50 | 10px | |
font.size.75 | 11px | |
font.size.100 | 12px | |
font.size.200 | 13px | |
font.size.300 | 14px | |
font.size.350 | 15px | |
font.size.400 | 16px | |
font.size.500 | 20px | |
font.size.600 | 24px | |
font.size.display-sm | clamp(22px, 3vw, 30px) | |
font.size.display-md | clamp(28px, 4.5vw, 40px) | |
font.size.display-lg | clamp(32px, 4.6vw, 52px) | |
font.size.display-xl | clamp(42px, 6.8vw, 84px) | |
font.weight.regular | 400 | |
font.weight.medium | 500 | |
font.weight.semibold | 600 | |
font.weight.bold | 700 | |
font.leading.tight | 1.1 | |
font.leading.snug | 1.4 | |
font.leading.normal | 1.6 | |
font.leading.relaxed | 1.7 | |
font.tracking.display | -0.03em | |
font.tracking.tight | -0.02em | |
font.tracking.normal | 0em | |
font.tracking.label | 0.08em | |
font.tracking.eyebrow | 0.12em | |
radius.none | 0px | |
radius.sm | 2px | |
radius.md | 6px | |
radius.lg | 10px | |
radius.full | 9999px | |
border-width.hairline | 1px | |
border-width.thick | 2px | |
duration.instant | 0ms | |
duration.fast | 100ms | |
duration.base | 200ms | |
duration.slow | 320ms | |
easing.standard | [0.2,0,0,1] | |
shadow.overlay-dark | {"color":"#000000a6","offsetX":"0px","offsetY":"8px","blur":"24px","spread":"0px"} | |
shadow.overlay-light | {"color":"#0000002e","offsetX":"0px","offsetY":"8px","blur":"24px","spread":"0px"} | |
shadow.raised-dark | {"color":"#ffffff1f","offsetX":"0px","offsetY":"0px","blur":"0px","spread":"1px"} | |
shadow.raised-light | [{"color":"#0000000f","offsetX":"0px","offsetY":"0px","blur":"0px","spread":"1px"},{"color":"#0000000f","offsetX":"0px","offsetY":"1px","blur":"2px","spread":"-1px"},{"color":"#0000000a","offsetX":"0px","offsetY":"2px","blur":"4px","spread":"0px"}] | |
size.control-sm | 32px | |
size.control-md | 40px | |
size.control-lg | 48px | |
size.target-min | 24px | WCAG 2.2 target size (minimum), 2.5.8. |
size.measure | 680px | Longest comfortable line of running text. |
size.container | 1200px |