# Design Tokens & Theming > Named design decisions that flow from source to every platform. Frontend architecture guidance from fearchitect, written by Abas Turabli and last reviewed 2026-06-21. Source: https://fearchitect.com/topics/design-tokens-theming Use it as reference for the task at hand. Before changing code, check this guidance against the codebase: where the code already makes a different, deliberate choice, flag the conflict instead of rewriting it. Library APIs move faster than this guide, so confirm exact signatures in the official docs linked at the end. ## Summary Design tokens are named values — colors, spacing, radii — stored once and transformed to CSS custom properties, iOS Swift, Android XML, or any output via a build tool such as Style Dictionary. A three-tier model (primitive → semantic → component) separates raw values from intent, making light/dark and brand swaps a single file change rather than a search-and-replace. ## Three-tier token model Tokens are organized in three layers. **Primitive** tokens are raw values: `--color-blue-500: #3b82f6`. They have no meaning beyond the value itself. **Semantic** tokens reference primitives and carry intent: `--color-action-primary: var(--color-blue-500)`. **Component** tokens scope a semantic to one component: `--button-bg: var(--color-action-primary)`. This indirection means swapping themes only touches the semantic layer — primitives and components stay unchanged. ## Diagram ```mermaid graph LR P["Primitive\n--color-blue-500"] --> S["Semantic\n--color-action-primary"] S --> C["Component\n--button-bg"] T["tokens.json\n(source of truth)"] --> B["Style Dictionary"] B --> CSS["CSS custom props"] B --> iOS["Swift / iOS"] B --> Android["Android XML"] ``` Tokens flow from a single JSON source through Style Dictionary to platform-specific outputs; the three tiers sit inside the CSS output. ## CSS custom properties theme swap Semantic tokens live on `:root`; the dark theme overrides them on `[data-theme='dark']`. Components reference only semantic tokens, so switching themes is one attribute change on ``. **Light/dark theme via CSS custom properties** ```css /* primitives — raw values, never used by components directly */ :root { --color-gray-50: #f9fafb; --color-gray-900: #111827; --color-blue-500: #3b82f6; } /* semantic tokens — default (light) theme */ :root { --color-surface: var(--color-gray-50); --color-text-primary: var(--color-gray-900); --color-action-primary: var(--color-blue-500); } /* dark theme override — only semantic layer changes */ [data-theme="dark"] { --color-surface: var(--color-gray-900); --color-text-primary: var(--color-gray-50); --color-action-primary: var(--color-blue-500); /* same primitive, different context */ } /* component — references semantic only */ .btn-primary { background: var(--color-action-primary); color: var(--color-surface); } ``` Toggle `data-theme="dark"` on `` and every component updates instantly — zero JS rerenders, zero class churn. ## Build-time vs runtime theming | | Approach | How it works | Perf cost | Flexibility | | --- | --- | --- | --- | --- | | **Build-time (CSS files)** | Style Dictionary emits one CSS file per theme at build | Zero runtime cost; ship only active theme | None | Theme switch requires page reload or separate stylesheet swap | | **Runtime (CSS custom props)** | One stylesheet; JS sets `data-theme` attribute on `` | Single CSS parse; no JS bundle cost per theme | Minimal | Instant switch; supports user preference + OS sync | | **Runtime (JS-in-CSS / CSS-in-JS)** | Theme object injected into component styles at render | Re-runs style computation per render; ~20 ms overhead at scale | High | Fully dynamic; token values can be computed at runtime | ## Watch out: Flash of unstyled theme (FOUT) When the active theme is set by JS after HTML parse, users see a flash of the default theme. Fix it by inlining a `