Skip to content
Rymflux Documentation
Esc
navigateopen⌘Jpreview
On this page

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:

  1. 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.
  2. Layer 2 — semantic tokens: the shadcn token set plus Rymflux additions (--ambient-color, --chrome/--chrome-foreground, --focus-ring). Declared on :root and [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.
  3. Layer 3 — Tailwind bindings: @theme inline maps Layer 2 onto utilities (bg-primary, text-muted-foreground, border-border, rounded-lg), so Tailwind classes always resolve to the active theme. The dark variant 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/@theme values 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-color rides 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 inline binding).
  • 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.css structure), §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)

Was this page helpful?