Skip to main content

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-ringvar(--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

TokenPurpose
--primary, --primary-hover, --primary-subtleThe only action colour
--secondary, --secondary-hover, --secondary-subtleHighlights and "current" markers. One per view
--bg, --bg-surface, --bg-elevatedThree elevation steps
--border, --border-strongStructure. Borders, not shadows, in dark
--text, --text-muted, --text-faint
--success, --warning, --danger, --info (+ -subtle)Semantic
--tag-1--tag-8Chips, 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";