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.

Layer 1

Primitives

Raw values — colors, radius, sizing, spacing, fonts.

--color-blazeorange-500: #ff5100--radius-radius-12: 0.75rem--spacing-space-16: 1rem
Layer 2

Modes

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)
Layer 3

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-serif
Axis

Spacing

data-spacing={compact, standard, wide}. Adjusts every --spacing-*, --size-*, and --font-size-* token.

--spacing-md: var(--spacing-space-12)
Axis

Curves

data-curves={sharp, slight, sweeping}. Adjusts every --curves-* token.

--curves-md: var(--radius-radius-12)
Axis

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.

mono
blazeorange
bluechill
blueribbon
bostonblue
cerise
concrete
crimson
electriclime
electricviolet
forestgreen
gossamer
lochmara
maitai
purpleheart
redviolet
rockspray
seagreen
teak
torchred
violeteggplant

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-primary

primary

--backgrounds-secondary

secondary

--backgrounds-tertiary

tertiary

--backgrounds-quaternary

quaternary

--backgrounds-quinary

quinary

Foregrounds

Aa
--foregrounds-primary

primary

Aa
--foregrounds-secondary

secondary

Aa
--foregrounds-tertiary

tertiary

Aa
--foregrounds-quaternary

quaternary

Aa
--foregrounds-quinary

quinary

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-bg

Surface (cards, popovers, page bg)

--container-bg-alt

Alt surface (subtle stripe)

--container-bg-inverse

Inverted surface

--container-border

Hairline border

--container-border-alt

Stronger border

--container-border-inverse

Inverted border

Aa
--container-fg

Body text

Aa
--container-fg-alt

Muted text

Aa
--container-fg-inverse

Inverted 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-bg

Resting

--interactive-bg-hover

Hover

--interactive-bg-active

Active

--interactive-bg-selected

Selected

--interactive-bg-disabled

Disabled

--interactive-bg-alt

Alt resting

--interactive-bg-alt2

Alt 2

--interactive-border

Focus / default border

--interactive-border-alt

Alt border

Aa
--interactive-fg

Foreground

Aa
--interactive-fg-alt

Muted / icon

Aa
--interactive-fg-active

Active fg

Aa
--interactive-fg-selected

Selected fg

Aa
--interactive-fg-disabled

Disabled 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-bg

Background

--action-primary-bg-hover

Hover

--action-primary-bg-active

Active

--action-primary-bg-selected

Selected

--action-primary-bg-disabled

Disabled

--action-primary-bg-inverse

Inverse

Aa
--action-primary-fg

Foreground

Aa
--action-primary-fg-active

Active fg

Aa
--action-primary-fg-disabled

Disabled fg

Aa
--action-primary-fg-inverse

Inverse fg

Action secondary

Outline button — secondary actions.

--action-secondary-border

Border

--action-secondary-bg-hover

Hover bg

--action-secondary-bg-active

Active bg

--action-secondary-bg-selected

Selected bg

--action-secondary-bg-disabled

Disabled bg

Aa
--action-secondary-fg

Foreground

Aa
--action-secondary-fg-alt

Alt fg

Aa
--action-secondary-fg-active

Active fg

Aa
--action-secondary-fg-inverse

Inverse fg

Action tertiary

Ghost button — tertiary / passive actions.

--action-tertiary-bg

Background

--action-tertiary-bg-hover

Hover

--action-tertiary-bg-active

Active

--action-tertiary-bg-selected

Selected

--action-tertiary-bg-alt

Alt bg

--action-tertiary-border

Border

Aa
--action-tertiary-fg

Foreground

Aa
--action-tertiary-fg-alt

Alt fg

Aa
--action-tertiary-fg-active

Active 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-bg

bg

--destructive-bg-alt

bg-alt

--destructive-bg-inverse

bg-inv

--destructive-border

border

Aa
--destructive-fg

fg

Aa
--destructive-fg-alt

fg-alt

--cautionary-* Warnings, attention needed

--cautionary-bg

bg

--cautionary-bg-alt

bg-alt

--cautionary-bg-inverse

bg-inv

--cautionary-border

border

Aa
--cautionary-fg

fg

Aa
--cautionary-fg-alt

fg-alt

--important-* High-emphasis flags

--important-bg

bg

--important-bg-alt

bg-alt

--important-bg-inverse

bg-inv

--important-border

border

Aa
--important-fg

fg

Aa
--important-fg-alt

fg-alt

--informative-* Neutral information

--informative-bg

bg

--informative-bg-alt

bg-alt

--informative-bg-inverse

bg-inv

--informative-border

border

Aa
--informative-fg

fg

Aa
--informative-fg-alt

fg-alt

--positive-* Success, confirmation

--positive-bg

bg

--positive-bg-alt

bg-alt

--positive-bg-inverse

bg-inv

--positive-border

border

Aa
--positive-fg

fg

Aa
--positive-fg-alt

fg-alt

Data viz

Eight series, each with main / tint / shade. Distinct hues at distinct lightness levels — readable in light, dim, and dark.

dataviz-1
dataviz-2
dataviz-3
dataviz-4
dataviz-5
dataviz-6
dataviz-7
dataviz-8

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-xxs
0.125rem
--spacing-xs
0.25rem
--spacing-sm
0.5rem
--spacing-md
0.75rem
--spacing-lg
1rem
--spacing-xl
1.25rem
--spacing-2xl
1.5rem
--spacing-3xl
2.5rem
--spacing-4xl
5rem

Typeset

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.

Step480px748px1024pxline-heightletter-spacing
step-641.81px53.62px68.66px1.2-0.03em
step-534.84px43.68px54.93px1.2-0.025em
step-429.03px35.59px43.95px1.2-0.02em
step-324.19px29.02px35.16px1.2-0.015em
step-220.16px23.66px28.13px1.375-0.01em
step-116.8px19.31px22.5px1.3750em
step-014px15.76px18px1.50em
step-neg111.67px12.87px14.4px1.50em
step-neg29.72px10.51px11.52px1.50em
step-neg38.1px8.59px9.22px1.50em

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 fox
step-5The quick brown fox
step-4The quick brown fox
step-3The quick brown fox
step-2The quick brown fox
step-1The quick brown fox
step-0The quick brown fox
step-neg1The quick brown fox
step-neg2The quick brown fox
step-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.

Aliascompactstandardwide
--font-size-xsstep-neg3step-neg2step-neg1
--font-size-smstep-neg2step-neg1step-0
--font-size-basestep-neg1step-0step-1
--font-size-lgstep-0step-1step-2
--font-size-xlstep-1step-2step-3
--font-size-2xlstep-2step-3step-4
--font-size-3xlstep-3step-4step-5
--font-size-4xlstep-4step-5step-6
--font-size-5xlstep-5step-6step-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-xl24px

Usage

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.