@aurea-uds/tokens
v0.6.0
Published
Design tokens for Aurea UDS: the DTCG source and the CSS custom properties generated from it.
Maintainers
Readme
Aurea foundations — what the tokens govern, and what they do not
Canonical answer to "which axis owns this value". Written in Phase 6 of the audit plan, because the audit found the foundations documented nowhere: density and theme both promised things no document defined, and a quarter of the tokens had no consumer (findings M2, M17, M18).
Counts live in STATE.md, generated and gated. This file holds the rules.
The axes
| Axis | Tokens | Unit | Notes |
|---|---|---|---|
| Colour | --<semantic>, --<family>-<step> | oklch / hex | semantic first; a ramp step is for building a semantic, not for using directly |
| Spacing | --space-0 … --space-24 | rem | scale with deliberate gaps — not every integer exists |
| Type size | --text-xs … --text-5xl | rem | |
| Type family | --font-ui, --font-editorial, --font-code | — | IBM Plex Sans / Serif / Mono |
| Type weight | --weight-regular … --weight-bold | number | created in Phase 6; the axis did not exist |
| Line height | --leading-none … --leading-relaxed | number | --leading-none exists for components whose HEIGHT is a contract |
| Letter spacing | --tracking-tight … --tracking-widest | em | created in Phase 6 |
| Radius | --radius-sm … --radius-card, --radius-control | px | --radius-card (22px) and --radius-control (999px) are identity — gated |
| Border | --border-width | px | |
| Shadow | --shadow-md, --shadow-lg | shadow | the xs/sm steps were removed in Phase 6: both were none and unused |
| Layer | --z-sticky … --z-toast | number | for OVERLAY stacking. Local z-index: 1/2/3 inside a component is not this axis |
| Motion | --duration-*, --ease-* | duration / bezier | |
| Icon size | --icon-sm … --icon-xl | rem | created in Phase 6; rem so glyphs follow text size |
| Touch target | --target-min | rem | 44px at a 16px root — WCAG 2.5.5/2.5.8 |
| Breakpoint | --breakpoint-sm … --breakpoint-2xl | px | @media cannot read var(), so the CSS repeats the literal and validate.py check 4b guards the scale |
| Density | see below | rem | |
New value on any of these axes comes from a token. Raw values in the core are ratcheted per
axis by validate.py check 12 against scripts/raw-px-baseline.json: the count may fall, never
rise.
Density contract — what actually changes
data-density="compact | comfortable | spacious" changes eight tokens, and only these:
--control-h-xs, --control-h-sm, --control-h-md, --control-h-lg, --control-h-xl,
--row-h, --card-pad, --section-gap.
So density changes control height, table row height, card padding and gap between sections.
It deliberately does not change: type size, the general spacing scale, layout gaps, radii, icon sizes or line heights. Density is about how tightly controls pack, not about a second type scale.
This was undocumented until Phase 6, and the promise "three densities" read as if it governed
everything. Every control must take its height from --control-h-* — three families did not,
and that is why a tab measured 8px shorter than the field beside it in spacious
(finding A1). tests/visual/geometry.spec.ts now enforces it.
Theme contract — and one asymmetry, on purpose
data-theme="dark | light". Dark is the base; light overrides.
The two themes are not symmetric, and this is the exception: light overrides six colours
that dark leaves at their base value —
danger-400, info-400, success-400, warning-400, oracle-300, oracle-400.
Reason: on a light surface those four semantic families need a deeper tone to reach 4.5:1,
while on dark the base value already does. Nothing is undefined in dark — it inherits from
base, so there is no missing-value bug. What was missing was this paragraph.
Identity does not change between themes: the primary yellow is oklch(0.795 0.184 86.047) in
both, and validate.py check 3 fails on the canonical value, not just the token name.
Units: rem for anything that should follow text
Type, spacing, icon sizes, control heights, sidebar width and topbar height are rem, so a reader who doubles the browser font size gets bigger controls instead of clipped text (WCAG 1.4.4, finding M12). The conversion from px was exact (px÷16), so nothing moved at the default root size — the 52 pixel baselines prove it.
Radii stay px: a corner is a physical shape, not text, and scaling it with type size makes a pill stop being a pill.
Tokens with no consumer
63 of 174 distinct names are not referenced by the core. That is not automatically waste:
| Group | Why it exists |
|---|---|
| --breakpoint-* (5) | @media cannot read var(). The CSS repeats the literal and check 4b guards the scale against them |
| --oracle-* (11) | consumed only by apps/docs/docs.css. oracle is vocabulary from the origin app, not the system (finding M9) — these leave with that page |
| --chart-* (4) | reserved for charts, which do not exist yet as components |
| ramp steps (6) | intermediate steps of the semantic families, kept so a consumer can build a variant |
| the rest | used only by docs.css — 14 in total |
The rule going forward: a token is born from a use, not from a scale that looks complete.
Phase 6 removed 26 that had no use anywhere (--aether-*, --ember-*, two none shadows, two
duplicate radii) and created 15 that were needed immediately.
Duplicates are references, not copies: --radius-full is {radius-control}, so the two names
cannot drift. Two tokens with the same number written by hand diverge the day one is adjusted.
Cascade
packages/core/dist/aurea.css wraps tokens and component CSS in @layer aurea. A
consumer's own CSS, written without a layer, therefore wins over the library regardless of
selector specificity — no !important, no specificity hunting.
Tokens are inside the layer with the core, not outside it. A custom property declaration
competes in the cascade like any other: with the tokens outside, the :root override that the
core declares inside a media query lost to the base value, and the sidebar jumped 48px.
An app that redefines a token in its own unlayered :root still wins over both.
