Frontend / Component libraries / 03_design_tokens_and_theming.md

Design Tokens and Theming at Scale

Updated 4 interview angles 5 min read source
On this page10
  1. What are design tokens and why use them?
  2. What are the token tiers?
  3. How do CSS custom properties power theming?
  4. What is Style Dictionary’s role?
  5. How do you do multi-tenant / white-label theming?
  6. Runtime vs build-time theming — trade-offs?
  7. Gotchas
  8. Related
  9. Further reading
  10. Interview angle

Design Tokens and Theming at Scale

Design tokens are named design decisions stored as data — color.brand.primary = #2563eb, space.4 = 16px — the single source of truth shared by design and every platform. They’re tiered: primitive (raw values) → semantic/alias (color.text.danger) → component (button.bg). The modern pipeline is Figma → tokens (JSON, increasingly the W3C DTCG format) → Style Dictionary → CSS custom properties / iOS / Android outputs. At runtime, CSS variables make theming (dark mode, multi-tenant) a matter of swapping values, not rebuilding.

What are design tokens and why use them?

Platform-agnostic, named values for the atomic design decisions (color, spacing, typography, radius, shadow, motion). Instead of hard-coding #2563eb in 50 places, you reference color.brand.primary. Benefits: one source of truth, consistent design, themeable, and design↔code stay in sync (designers change the token, code picks it up). They scale a design system across products and platforms.

What are the token tiers?

Tier Example Purpose
Primitive / global blue.500 = #2563eb, space.4 = 16px the raw palette/scale; no meaning
Semantic / alias color.text.danger → red.600 intent; what it’s for
Component button.primary.bg → color.action per-component, optional

Components reference semantic tokens, semantic reference primitives. That indirection is what lets you re-theme: dark mode remaps semantic→primitive without touching components.

How do CSS custom properties power theming?

Define tokens as CSS variables, theme by overriding them on a scope:

css
:root { --color-bg: #fff; --color-text: #111; }
[data-theme="dark"] { --color-bg: #0b0b0b; --color-text: #f5f5f5; }

.card { background: var(--color-bg); color: var(--color-text); }

Flip data-theme on <html> and the whole tree re-themes — no re-render, no prop drilling, cascades to portals. For OS sync, default from @media (prefers-color-scheme: dark) and let an explicit toggle override. See CSS.

What is Style Dictionary’s role?

It’s a build tool that transforms one token source (JSON/YAML) into many platform outputs: CSS variables, SCSS, JS/TS constants, iOS (Swift), Android (XML). You author tokens once; it generates per-platform artifacts with transforms (e.g., px→pt, hex→rgba). This is how a token change propagates everywhere from a single edit, and how design and native apps stay consistent.

How do you do multi-tenant / white-label theming?

Keep components referencing semantic tokens; per tenant, provide a different set of token values (a theme bundle) injected as CSS variables at runtime (from tenant config) or built per tenant. Because components never hard-code values, a new tenant is “a new token file,” not a code change. Runtime injection (set CSS vars from an API response on load) supports many tenants from one build.

json
// tokens/acme.json — the whole of a new tenant.
{
  "color": {
    "brand": { "500": { "value": "#0b6bcb" } },
    "surface": { "value": "{color.neutral.50}" }
  }
}
ts
// Applied at runtime, before first paint.
for (const [name, value] of Object.entries(tenantTokens)) {
  document.documentElement.style.setProperty(`--${name}`, value)
}

The reference {color.neutral.50} is the tier system doing its job: the tenant overrides brand colours and inherits everything else. Onboarding a tenant is a JSON file and no deploy.

Runtime vs build-time theming — trade-offs?

  • Build-time (compile a theme into CSS) — zero runtime cost, but a new theme needs a build/deploy; bad for many tenants.
  • Runtime (CSS variables swapped live) — instant theme switching and unlimited tenants from one bundle; tiny runtime cost, and you must avoid FOUC by setting the theme before first paint (inline script in <head>). CSS variables make runtime theming cheap, so it’s the common choice.

Gotchas

  • Skipping the semantic tier — components referencing primitives (blue.500) directly can’t be re-themed; you lose the whole point. Always go through semantic tokens.
  • FOUC / theme flash — reading theme from localStorage in React after hydration flashes the wrong theme; set data-theme via a blocking inline script in <head> before paint (Rendering Modes — Senior Interview Prep).
  • Tokens in JS vs CSS — JS token objects can’t be overridden by the cascade and bloat the bundle; prefer CSS variables for themeable values, JS tokens only where you must compute.
  • Naming churn — renaming tokens breaks consumers; treat token names as an API (deprecate, don’t silently rename).
  • Contrast across themes — a token palette that passes contrast in light mode may fail in dark mode; verify both (WCAG and the POUR Principles).
  • DTCG format is still stabilizing — the W3C Design Tokens spec is in progress; tooling support varies.

Further reading

Interview angle 4

  • “What is a design token, and why three tiers?” - a named design decision. Primitives are raw values (blue-500), semantic tokens give them meaning (color-action), and component tokens scope them further (button-bg). Components reference the semantic layer, so re-theming remaps values without touching a single component.
  • “Why CSS custom properties rather than a JS theme object?” - because theming becomes a value swap on a scope. The cascade handles it, it reaches content in portals that sit outside your React tree, and switching themes causes no re-render at all - a JS theme object re-renders every consumer.
  • “How do you keep tokens consistent across platforms?” - one source of truth compiled to each target, which is what Style Dictionary does: CSS variables, a JS object, iOS and Android resources from the same JSON. A token edit then propagates everywhere instead of being reapplied by hand.
  • “How do you support many tenants from one build?” - runtime theming with CSS variables, scoped per tenant. The one thing to handle is the flash: apply the stored theme in a small synchronous script before first paint, or the user sees the default theme for a frame.