ADR-0014: Semantic theme tokens, not hex literals

  • Status: Accepted
  • Date: 2026-06-05
  • Scope: core (any frontend with theming / dark mode)

Context

A hex literal in a styled-component is a decision that bypasses the design
system: it won’t flip in dark mode, won’t update when the palette evolves, and
encodes what color instead of what it means. Dark-mode parity dies one
hardcoded #fff at a time.

Decision

  • Semantic colors come from theme tokens, never hex literals. Components
    say theme.colors.surface / incomeFg / dangerFg — meaning, not value.
    The theme object (src/styles/theme.ts) is the single source of truth,
    mirroring the app’s design-system skill
    (0015-design-system-as-single-visual-source).
  • Dark mode is a token swap, not a component concern. darkTheme overrides
    the palette; components are written once. A surface that only looks right in
    one mode is a bug.
  • Status colors are semantic pairs (incomeFg/incomeBg, dangerFg,
    warningFg, …) that adapt per mode — green-means-income is a token, not a
    literal.
  • Transient props use the $ prefix ($active, $variant) so style-only
    props never leak to the DOM.
  • Tolerated literals: values genuinely identical across modes and owned by the
    brand (e.g. Expenses’ primary violet gradient) — rare, deliberate, commented.
  • Theme-aware tints derive from tokens (e.g. color-mix(in oklab, …) over
    token colors), not from new hardcoded shades.

Consequences

  • Palette evolution and dark-mode fixes are theme edits, not component sweeps.
  • A hex literal in review is an immediate, objective smell.