Design tokens
How prfct's tokens are structured, named, generated and exported — as CSS variables, Tailwind utilities and W3C design-token JSON.
Tokens are the contract between design and code. prfct's tokens are defined once, as data, and everything else — CSS, Tailwind utilities, design-tool files and this documentation — is generated from them. Nothing can drift, because nothing is written twice.
Architecture
Tokens come in three tiers. Each tier only references the tier below it.
Components use semantic tokens wherever a role exists and scale steps where a component needs a specific role (a badge's bg-brand-3 text-brand-11). They never use raw values.
Naming
--{scale}-{step} --brand-9, --gray-12, --red-3
--{scale}-contrast --amber-contrast (text on step 9)
--{role} --background, --primary, --border, --ring
--{role}-foreground --primary-foreground, --muted-foreground
--duration-{name} --duration-fast, --duration-spring
--ease-{name} --ease-enter, --ease-spring
--z-{layer} --z-popover, --z-tooltipIn Tailwind, every token is a utility: bg-brand-3, text-muted-foreground, rounded-xl, shadow-floating, ease-spring, duration-fast, z-popover, text-heading-md.
Source of truth
lib/color/oklch.ts OKLCH ↔ sRGB, gamut mapping, WCAG and APCA contrast
lib/color/scale.ts 12-step scale generator with solved text steps
lib/tokens/palette.ts hues and neutral presets
lib/tokens/semantic.ts semantic roles → scale steps, per mode
lib/tokens/foundations.ts typography, radius, shadows, motion, layers
scripts/build-tokens.ts → styles/tokens.css + public/tokens/*.jsonpnpm tokens # regenerate after changing any file in lib/tokensExports
Colors are exported in OKLCH with an sRGB hex fallback, so design tools that don't speak OKLCH still get exact values:
{
"color": {
"brand": {
"9": {
"$type": "color",
"$value": {
"colorSpace": "oklch",
"components": [0.515, 0.24, 267],
"hex": "#2c4fee"
}
}
}
},
"semantic": {
"ring": { "$type": "color", "$value": "{color.brand.9}" }
}
}Adding a token
- Add it to the relevant file in
lib/tokens. - Run
pnpm tokens. - If it's a new utility name (a new type role, shadow or duration), register it in
lib/utils.tssocn()merges it correctly — the test suite fails until you do. - Document it: the foundations pages render straight from
lib/tokens, so most tables update themselves.