Design tokens
tokens/ is the source of truth. Style Dictionary compiles DTCG JSON into CSS custom properties:
tokens/primitives/palette.json private — never emitted
tokens/base/{typography,spacing,motion}.json -> base.css (:root)
tokens/themes/dark.json -> dark.css (:root — dark is the default)
tokens/themes/light.json -> light.css ([data-theme="light"])
base + dark -> tailwind.css (@theme inline)
Edit the JSON, run npm run tokens. The generated CSS is committed so the package builds without the token step — never edit it by hand. (CI regenerates and fails if the committed output is stale.)
Two layers
primitives/palette.json holds the raw ramps (blue.500, neutral.850). Semantic tokens reference them and never hold a hex of their own:
{
"colors": {
"brand": { "primary": { "$value": "{palette.blue.500}" } },
"state": { "success": { "$value": "{palette.green.500}" } },
"surface": { "bg-surface": { "$value": "{palette.neutral.900}" } }
}
}
Change the brand blue in one place and it reaches --primary, --primary-hover and --tag-1, in both themes.
The palette is private — filtered out of every output, so --neutral-900 never becomes part of your API. References to it are inlined as literals; references to tokens that are emitted (like --focus-ring → var(--bg), var(--primary)) stay as var() and keep re-resolving per theme.
Grouping doesn't leak into the CSS
colors.brand.primary still emits --primary, not --colors-brand-primary. The variable names are the public API — you override them — so the sources can be reorganised without a breaking rename.
The tokens
Colour
| Token | Purpose |
|---|---|
--primary, --primary-hover, --primary-subtle | The only action colour |
--secondary, --secondary-hover, --secondary-subtle | Highlights and "current" markers. One per view |
--bg, --bg-surface, --bg-elevated | Three elevation steps |
--border, --border-strong | Structure. Borders, not shadows, in dark |
--text, --text-muted, --text-faint | |
--success, --warning, --danger, --info (+ -subtle) | Semantic |
--tag-1 … --tag-8 | Chips, chart series. Assign by index, never semantically |
--focus-ring, --focus-ring-input |
Type
Geist for UI, Geist Mono for code, data values and uppercase meta-labels. --text-xs (11px) through --text-3xl (36px); 14px (--text-base) is the workhorse.
Space, radii, motion
A 4px grid (--space-1 … --space-12). Soft radii — --radius-sm 6px for controls, --radius-md 8px for cards, --radius-lg 10px for modals; pill is reserved for Badge, TagChip and Avatar. Controls are 28/36/44px. Motion is --duration-fast 120ms, --duration-base 180ms, --duration-slow 260ms, all on --ease-out.
Overriding
Any token, anywhere:
:root {
--primary: #8b5cf6;
--radius-md: 4px; /* sharper cards */
}
[data-theme="light"] {
--bg: #ffffff;
}
For the variables without the component styles:
import "@kairosis/eidos/tokens.css";