ADR-015: Design token source — three-layer oklch CSS-variable architecture
ADR-015: Design token source — three-layer oklch CSS-variable architecture
Status
Accepted
Context
The UI system (ui-design-system.md) needs a single home for color, typography,
spacing, and radius, plus a theming model: four launch themes (Midnight, Dawn,
Cascade, Ember) × two variants (dark/light), with dark as the app default and
the ability to switch themes at runtime without a layout change. The component
base is shadcn-svelte (Rhea style), which is CSS-variable-driven (Tokens
bind to Tailwind v4 via @theme inline in oklch).
The design rule (ui-design-system.md §1): a theme is only a token override — it never adds layout or typography rules — and components must never contain inline color or length literals (frontend-architecture.md §15). The open question is where token values are defined and how themes override them.
Decision
Design tokens live in src/lib/styles/tokens.css (the only place color,
spacing, radius, and typography values are defined), organized in three layers:
- Layer 1 — primitives: a raw oklch palette (
--color-teal-500,--color-neutral-*, per-theme hues). Never referenced directly by components; exists so semantic tokens can point at stable values and themes can restyle by overriding Layer 2 only. - Layer 2 — semantic tokens: the shadcn token set plus Rymflux additions
(
--ambient-color,--chrome/--chrome-foreground,--focus-ring). Declared on:rootand[data-theme="dark"](dark is the CSS default, so first paint is correct before JS runs) and overridden on[data-theme="light"]. Components reference only Layer 2. - Layer 3 — Tailwind bindings:
@theme inlinemaps Layer 2 onto utilities (bg-primary,text-muted-foreground,border-border,rounded-lg), so Tailwind classes always resolve to the active theme. Thedarkvariant is wired via@custom-variant dark (&:is([data-theme="dark"] *)).
Runtime themes (ui-design-system.md §4) are applied by setting Layer 2
properties on document.documentElement from a theme’s variants map — a
theme is purely a Layer 2 override; it never adds layout or typography rules.
Alternatives considered
- Tailwind config-only tokens (
tailwind.config.ts/@themevalues with no semantic layer). Single file, but ties tokens to Tailwind: runtime theme switching requires regenerating/reloading config, and non-Tailwind consumers (plain CSS, canvas) can’t read them. Rejected. - Design-token pipeline (Style Dictionary / Tokens Studio). Build-time generation to JSON/CSS/TS. Powerful for multi-platform, but v1 is a single Tauri desktop app — the pipeline is overhead and separates the source of truth from the CSS that ships.
- CSS Modules / component-scoped styles. Locality at the cost of shared theming; token values would be duplicated per module. Rejected — violates the “single home for values” rule (frontend-architecture.md §15).
- Runtime JS theme objects as the source of truth. Theming would require JS before any styling; first paint would flash the wrong theme. Rejected in favor of CSS-defaults + runtime override (dark default in the stylesheet).
Consequences
Positive:
- One file owns every value; components carry no inline color/length literals.
- Themes are pure Layer 2 overrides — the four launch themes share one layout and typography system by construction.
- Dark default in the stylesheet means correct first paint before JS (no theme flash on load).
- The semantic layer gives a stable contrast-checking surface: a11y AA checks target the Layer 2 pairs in both variants (ui-design-system.md §10).
--ambient-colorrides the same token channel, so ambient tinting (ADR-016) needs no new theming machinery.
Negative:
- Three layers of indirection (primitives → semantic → Tailwind) is more files than a flat config; contributors must learn the rule “reference Layer 2, never Layer 1.”
- Adding a token touches up to three places (primitive, semantic,
@theme inlinebinding). - Custom properties are runtime-string; type-safety of token names relies on the Tailwind bindings and review, not the compiler.
References
ui-design-system.md§3 (design tokens), §3.1 (token architecture), §3.3 (semantic token set), §3.7 (app.cssstructure), §4 (theming), §6.3 (ambient channel)frontend-architecture.md§15 (conventions — no inline literals)- NFR-A11Y-1, NFR-A11Y-2 (contrast targets checked against Layer 2 pairs)
- ADR-016 (ambient dominant color — consumed through
--ambient-color)