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
saytheme.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.
darkThemeoverrides
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.
Related
- 0015-design-system-as-single-visual-source — where the tokens come from
- 0001-feature-vertical-slices —
styles.tsis where tokens are consumed