Design Tokens
Three layers of CSS custom properties plus two adjustment axes. Every component consumes the top semantic layer; lower layers cascade through attribute selectors on the document root.
Architecture
Three layers feed each other, then two axes (spacing, curves) adjust component-level density and corner sharpness independently of theme and mode.
Primitives
Raw values — colors, radius, sizing, spacing, fonts.
--color-blazeorange-500: #ff5100--radius-radius-12: 0.75rem--spacing-space-16: 1remModes
data-mode={light, dim, dark}. Sets backgrounds, foregrounds, and per-palette aliases.
--backgrounds-primary: var(--color-mono-0)--foregrounds-primary: var(--color-mono-950)--blazeorange-main: var(--color-blazeorange-500)Themes
data-theme={minimal, <your custom themes>}. Component-level semantic tokens — what components actually use.
--action-primary-bg: var(--foregrounds-primary)--container-bg: var(--backgrounds-primary)--font-heading: "Inter", sans-serifSpacing
data-spacing={compact, standard, wide}. Adjusts every --spacing-*, --size-*, and --font-size-* token.
--spacing-md: var(--spacing-space-12)Curves
data-curves={sharp, slight, sweeping}. Adjusts every --curves-* token.
--curves-md: var(--radius-radius-12)Typeset
data-typeset={small, medium, large}. Pins the fluid type scale to one column. Omit it — the default — and every step follows the viewport.
--font-size-step-0: 1.125rem Primitives
Layer 1 lives in :root and never changes. Every other layer ultimately resolves to a primitive.
Color palettes
21 palettes with 11 shades each (50 through 950); mono adds a 0 for pure white.
The CSS declares every shade in oklch(), with the original hex kept in a trailing comment. Rendering is unchanged — the values are the same colors, expressed in a perceptually uniform space so that generated ramps and contrast maths stay predictable. The TypeScript export still hands back hex strings.
monoblazeorangebluechillblueribbonbostonblueceriseconcretecrimsonelectriclimeelectricvioletforestgreengossamerlochmaramaitaipurpleheartredvioletrocksprayseagreenteaktorchredvioleteggplant Radius / sizing / spacing
Modes
Layer 2 reads data-modeand remaps backgrounds, foregrounds, and every palette's contextual alias. The Modes page has the full rundown — the visualisations below show the active mode for each surface tier.
Backgrounds
--backgrounds-primaryprimary
--backgrounds-secondarysecondary
--backgrounds-tertiarytertiary
--backgrounds-quaternaryquaternary
--backgrounds-quinaryquinary
Foregrounds
--foregrounds-primaryprimary
--foregrounds-secondarysecondary
--foregrounds-tertiarytertiary
--foregrounds-quaternaryquaternary
--foregrounds-quinaryquinary
Themes
Layer 3 reads data-theme and defines the semantic tokens components actually consume. Below: every group, every variant, rendered in the active theme + mode.
Container
--container-bgSurface (cards, popovers, page bg)
--container-bg-altAlt surface (subtle stripe)
--container-bg-inverseInverted surface
--container-borderHairline border
--container-border-altStronger border
--container-border-inverseInverted border
--container-fgBody text
--container-fg-altMuted text
--container-fg-inverseInverted text
Interactive
Inputs, selectable rows, hoverable items — anything the user targets with pointer or keyboard. --interactive-border is the system-wide focus-ring colour.
--interactive-bgResting
--interactive-bg-hoverHover
--interactive-bg-activeActive
--interactive-bg-selectedSelected
--interactive-bg-disabledDisabled
--interactive-bg-altAlt resting
--interactive-bg-alt2Alt 2
--interactive-borderFocus / default border
--interactive-border-altAlt border
--interactive-fgForeground
--interactive-fg-altMuted / icon
--interactive-fg-activeActive fg
--interactive-fg-selectedSelected fg
--interactive-fg-disabledDisabled fg
Action
Three tiers — primary (CTA), secondary (outline), tertiary (ghost) — each with the same shape: bg, fg, and border across default / hover / active / selected / disabled / inverse states.
Action primary
Filled CTA — the most prominent action.
--action-primary-bgBackground
--action-primary-bg-hoverHover
--action-primary-bg-activeActive
--action-primary-bg-selectedSelected
--action-primary-bg-disabledDisabled
--action-primary-bg-inverseInverse
--action-primary-fgForeground
--action-primary-fg-activeActive fg
--action-primary-fg-disabledDisabled fg
--action-primary-fg-inverseInverse fg
Action secondary
Outline button — secondary actions.
--action-secondary-borderBorder
--action-secondary-bg-hoverHover bg
--action-secondary-bg-activeActive bg
--action-secondary-bg-selectedSelected bg
--action-secondary-bg-disabledDisabled bg
--action-secondary-fgForeground
--action-secondary-fg-altAlt fg
--action-secondary-fg-activeActive fg
--action-secondary-fg-inverseInverse fg
Action tertiary
Ghost button — tertiary / passive actions.
--action-tertiary-bgBackground
--action-tertiary-bg-hoverHover
--action-tertiary-bg-activeActive
--action-tertiary-bg-selectedSelected
--action-tertiary-bg-altAlt bg
--action-tertiary-borderBorder
--action-tertiary-fgForeground
--action-tertiary-fg-altAlt fg
--action-tertiary-fg-activeActive fg
Status
Five intent families — each with the same six-variant shape. Hover any swatch to see the full token name.
--destructive-* — Errors, dangerous actions
--destructive-bgbg
--destructive-bg-altbg-alt
--destructive-bg-inversebg-inv
--destructive-borderborder
--destructive-fgfg
--destructive-fg-altfg-alt
--cautionary-* — Warnings, attention needed
--cautionary-bgbg
--cautionary-bg-altbg-alt
--cautionary-bg-inversebg-inv
--cautionary-borderborder
--cautionary-fgfg
--cautionary-fg-altfg-alt
--important-* — High-emphasis flags
--important-bgbg
--important-bg-altbg-alt
--important-bg-inversebg-inv
--important-borderborder
--important-fgfg
--important-fg-altfg-alt
--informative-* — Neutral information
--informative-bgbg
--informative-bg-altbg-alt
--informative-bg-inversebg-inv
--informative-borderborder
--informative-fgfg
--informative-fg-altfg-alt
--positive-* — Success, confirmation
--positive-bgbg
--positive-bg-altbg-alt
--positive-bg-inversebg-inv
--positive-borderborder
--positive-fgfg
--positive-fg-altfg-alt
Data viz
Eight series, each with main / tint / shade. Distinct hues at distinct lightness levels — readable in light, dim, and dark.
Spacing scale
data-spacing drives three families — --spacing-* (gaps / padding), --size-* (component heights), and --font-size-* (text). The first two shift by one primitive rung; --font-size-* shifts by one typeset step. Bars below show the spacing family at the active data-spacing setting.
--spacing-xxs0.125rem--spacing-xs0.25rem--spacing-sm0.5rem--spacing-md0.75rem--spacing-lg1rem--spacing-xl1.25rem--spacing-2xl1.5rem--spacing-3xl2.5rem--spacing-4xl5remTypeset
Type is the one scale that is not a set of static rungs. Eluan ships a fluid typeset of ten steps — --font-size-step-6 down to --font-size-step-neg3 — and every step interpolates with the viewport, so a page reads well on a phone and on a desktop without a single media query in your app. The old numeric primitives (--font-size-12 … --font-size-60) are gone.
The ten steps
Each step is anchored at three viewport widths, matching the s / m / l viewport primitives. The small column steps by a minor third (×1.2), the large column by a major third (×1.25) — bigger screens get more typographic contrast.
| Step | 480px | 748px | 1024px | line-height | letter-spacing |
|---|---|---|---|---|---|
| step-6 | 41.81px | 53.62px | 68.66px | 1.2 | -0.03em |
| step-5 | 34.84px | 43.68px | 54.93px | 1.2 | -0.025em |
| step-4 | 29.03px | 35.59px | 43.95px | 1.2 | -0.02em |
| step-3 | 24.19px | 29.02px | 35.16px | 1.2 | -0.015em |
| step-2 | 20.16px | 23.66px | 28.13px | 1.375 | -0.01em |
| step-1 | 16.8px | 19.31px | 22.5px | 1.375 | 0em |
| step-0 | 14px | 15.76px | 18px | 1.5 | 0em |
| step-neg1 | 11.67px | 12.87px | 14.4px | 1.5 | 0em |
| step-neg2 | 9.72px | 10.51px | 11.52px | 1.5 | 0em |
| step-neg3 | 8.1px | 8.59px | 9.22px | 1.5 | 0em |
Between the anchors the value is a two-segment clamp(): small→medium from 480px to 748px, then medium→large from 748px to 1024px. Below 480px every step pins to its small value; above 1024px, to its large value. The bend at the medium anchor is why there are two segments rather than one straight line.
Two companion tokens travel with every size and should always be applied together — --line-height-step-* and --letter-spacing-step-*. Tracking is expressed in em, so it scales with the fluid size automatically; headings tighten progressively to -0.03emat step 6, which is the optical correction a dedicated “Tight” cut would give without a second font payload.
step-6The quick brown foxstep-5The quick brown foxstep-4The quick brown foxstep-3The quick brown foxstep-2The quick brown foxstep-1The quick brown foxstep-0The quick brown foxstep-neg1The quick brown foxstep-neg2The quick brown foxstep-neg3The quick brown fox In React, reach for the Typography component instead — it applies all three at once and picks a sensible element per variant.
Pinning with data-typeset
data-typeset is the fifth axis, and the only one whose default is to be absent. Leave it off and the steps follow the viewport; set it and every step freezes to one column.
EluanProvider exposes this as defaultTypeset plus typeset / setTypeset on useEluanTheme(). Only explicit pins are persisted — "auto" is the absence of a preference, so it clears the eluan:typeset key rather than storing a value. See the Theming guide.
Density re-indexing
The --font-size-* aliases still exist, but they no longer hold values — each one selects a step, and data-spacing shifts which one, exactly as it shifts --spacing-* and --size-* by one primitive rung. Wide saturates at the top of the ladder (4xl and 5xl both land on step 6), mirroring compact, where xs and sm have always shared a rung.
| Alias | compact | standard | wide |
|---|---|---|---|
| --font-size-xs | step-neg3 | step-neg2 | step-neg1 |
| --font-size-sm | step-neg2 | step-neg1 | step-0 |
| --font-size-base | step-neg1 | step-0 | step-1 |
| --font-size-lg | step-0 | step-1 | step-2 |
| --font-size-xl | step-1 | step-2 | step-3 |
| --font-size-2xl | step-2 | step-3 | step-4 |
| --font-size-3xl | step-3 | step-4 | step-5 |
| --font-size-4xl | step-4 | step-5 | step-6 |
| --font-size-5xl | step-5 | step-6 | step-6 |
So the two axes compose: density picks the rung, and the typeset (or the viewport) decides how big that rung is.
Curves scale
data-curves drives --curves-*. Six levels, backed by radius primitives.
--curves-xxs2px--curves-xs4px--curves-sm8px--curves-md12px--curves-lg16px--curves-xl24pxUsage
Tokens are plain CSS custom properties — reference them anywhere a CSS value is accepted. No build step, no plugin, no configuration: in a stylesheet, in a style attribute, or in a stylex.create declaration.
Because the tokens stay as var() references rather than being inlined at build time, they re-resolve whenever the theme, mode or density axis changes — no recompile. For one-off scoped overrides, set CSS variables inline:
For runtime mode / theme switching with persistence and font preloading, use the EluanProvider — see the Theming guide.