CSS Custom Properties as Design Tokens: A Complete Setup Guide

Sourabh R.

Founder

8 min read

You're 3 hours into a design review when the client says "can we try the button in teal instead of blue?" Your designer updates the Figma file. Your developer opens 14 component files. Twenty minutes later, 3 of them still say blue. This is what happens when your design decisions don't map to CSS custom properties as design tokens — you're managing color in a hundred places instead of one.

What makes a token system actually work

A mature token system has three levels, each serving a different purpose:

  • Primitive tokens — the raw values. --color-blue-500: #3b82f6
  • Semantic tokens — purpose-driven aliases. --color-primary: var(--color-blue-500)
  • Component tokens — scoped to specific components. --btn-bg: var(--color-primary)
Three-tier token structure
/* Primitives */
--color-blue-500: #3b82f6;
--space-4: 1rem;
/* Semantic */
--color-primary: var(--color-blue-500);
--spacing-section: var(--space-4);
/* Component */
--btn-primary-bg: var(--color-primary);

Dark mode in a CSS variables design system

This is where the semantic layer pays off in a CSS variables design system. Dark mode requires changing the semantic token values, not touching a single component. Every component that references --color-surface or --color-text automatically switches — no component-level changes needed.

:root {
--color-surface: #ffffff;
--color-text: #0f172a;
}
[data-theme="dark"], .dark {
--color-surface: #020617;
--color-text: #f8fafc;
}

Naming conventions that hold up as teams grow

  • Use a --category-variant-state pattern: --color-primary-hover
  • Never name by value: --blue is meaningless the moment you change the blue
  • Name by role: --color-action communicates intent regardless of which hex value sits behind it

Auditing token resolution live

Use onHover's Element Inspector — built into the Chrome extension — to see computed CSS custom property values on any element. Hover the element, open the computed panel, scroll to the custom properties section — you'll see the resolved chain from component token all the way back to the primitive value.

Figma to code with the design token workflow

Tokens Studio (formerly Figma Tokens) lets you define tokens in Figma and export them as JSON. Style Dictionary then transforms that JSON into CSS custom properties, Tailwind config, or whatever format your stack needs. One definition in Figma becomes every output format your codebase requires — no manual translation between design and code.

The discipline that makes this work: every design decision in Figma must map to a token. If a designer picks a one-off hex value, that's a process failure — the fix is to add a token, not to hardcode a value in CSS.

Building the token system: where to start

The most common mistake we see is teams starting with component tokens before primitives are defined. You end up with component tokens that reference hardcoded hex values instead of primitives — which is only marginally better than hardcoding directly in components. You've added a layer of indirection without actually building the system.

The right order matters. Define all primitive color tokens first — the full palette, all weights, every neutral and brand color your product uses. Then create semantic tokens that reference only primitives. --color-primary references --color-blue-500, never a hex directly. Then define component tokens that reference only semantic tokens. --btn-primary-bg references --color-primary, never a primitive directly.

When this chain is intact, the teal-instead-of-blue request from the opening example becomes a one-line change. --color-primary gets pointed at --color-teal-500 instead of --color-blue-500. Every component token that traces back to --color-primary updates automatically. No file hunting, no three-still-say-blue problem.

Any place where a component or semantic token reaches past its layer to reference a raw value is a crack in the system. It'll cause maintenance pain as the codebase scales, and it will cause confusion when someone audits the token structure later and finds the chain broken. Keep each layer referencing only the layer below it — that constraint is the whole point.

Implementing tokens in React and Tailwind projects

CSS custom properties work in any React project that loads a global CSS file. No library required. The primitives and semantics live in :root, and components reference them through the cascade. That's it.

The challenge is Tailwind. Most Tailwind projects do all styling through utility classes, which means CSS custom properties sitting in :root aren't automatically available as utilities. The solution is Tailwind's theme extension in the config:

tailwind.config.js — token bridge
theme: {
extend: {
colors: {
primary: 'var(--color-primary)',
surface: 'var(--color-surface)',
}
}
}

That configuration makes Tailwind generate bg-primary, text-primary, and border-primary classes that resolve to your token value. Change the CSS custom property and all Tailwind classes that reference it update automatically — no Tailwind config changes, no rebuild of the config, just a CSS cascade update.

This is actually how we handle theming in onHover's web properties. Tokens are defined in CSS, referenced in the Tailwind config, consumed as utility classes throughout the codebase. The token is the single source of truth. Tailwind is the consumption mechanism. When a color needs to change, it changes in one place — the CSS token — and propagates everywhere Tailwind is using it.

Token debugging: finding where a value actually comes from

CSS custom property chains can get deep. A button's background might resolve through --btn-primary-bg--color-action--color-brand-500 → the actual hex value. That's three hops. In a large codebase with many components, chains like this are normal. Tracing them manually is tedious.

The computed styles panel in DevTools shows resolved values but doesn't show the chain. You see the final hex, but you don't know which tokens got you there. If a button is rendering the wrong color, the computed styles panel tells you the wrong color it's rendering — not which token in the chain is responsible.

The onHover Element Inspector resolves this by showing the full token chain for any element you hover. Open the onHover Chrome extension, hover the button, open the computed panel, find the --btn-primary-bg property — the inspector shows its value, and for custom property references, shows the chain it resolved through. Each hop in the chain is visible.

This is particularly useful when a token is being overridden somewhere in the cascade and you need to find where. A component-level override, a scoped theme, a third-party stylesheet — any of these can intercept the resolution chain. The onHover inspector shows exactly which selector and which file introduced the value you're actually seeing rendered, so you can fix the override instead of guessing at the cascade order.

Token override hunting

If a component's color looks wrong and you're not sure why, the chain view in onHover's computed panel shows which selector is winning the cascade. Third-party component libraries that define their own custom properties are a common source of unexpected overrides — the chain view makes this visible in seconds.

Tokens across platforms: web, iOS, and Android

Design tokens aren't a CSS concept. They're a product concept that CSS happens to implement well through custom properties. The same primitive and semantic token structure that drives your web UI can drive native iOS components (via Swift package), Android (via Kotlin/XML resource files), React Native (via StyleSheet constants), and even marketing email templates (via inline CSS generation from the same JSON source).

Style Dictionary — the most mature token transformation tool in the ecosystem — handles all of these targets from a single JSON token definition. You define your tokens once. Style Dictionary transforms them into platform-native formats for every target your product ships to. iOS gets Swift constants. Android gets XML color resources. Web gets CSS custom properties. All from the same source file.

The implication for product teams is significant. A brand color change that used to require PRs in four separate repositories, coordinated across four different teams working in four different languages, can become a single token change that propagates everywhere through automated CI pipelines. One PR. One review. One merge. Every platform updated.

That's the end goal of a mature token system — not just "consistent buttons across your React components" but genuine cross-platform consistency with one source of truth. The web CSS token work is the foundation. Expanding it to other platforms is mostly a Style Dictionary configuration problem, not a new system problem.

Design tokens sit at the intersection of design and engineering, and the onHover for Designers page covers the inspection and handoff tools that make working across that boundary faster.

Frequently asked questions

What are CSS custom properties (CSS variables)?
CSS custom properties are variables defined in CSS that store values you can reuse throughout a stylesheet. They're defined with a double-dash prefix (--color-primary: #3b82f6) and accessed with var() (color: var(--color-primary)). Unlike preprocessor variables (Sass, Less), CSS custom properties are live: they're part of the cascade, can be scoped to specific elements, can be changed with JavaScript at runtime, and update all references when changed.
What is a design token in CSS?
A design token is a named design decision — a specific value for color, spacing, typography, or other visual properties — that is stored separately from component code and used across both design tools and code. CSS custom properties are the natural implementation layer for design tokens: --color-surface-primary: #0f172a captures a design decision (the primary surface color) as a named token that both Figma designers and CSS developers can reference by name rather than raw value.
How do I use CSS variables for dark mode?
Define semantic tokens for light mode at :root and override them inside a dark mode class or prefers-color-scheme media query. Example: :root { --color-text: #0f172a; --color-bg: #ffffff; } @media (prefers-color-scheme: dark) { :root { --color-text: #f1f5f9; --color-bg: #0f172a; } }. Components that use var(--color-text) and var(--color-bg) automatically switch between modes without any per-component dark mode overrides.
What is the difference between CSS variables and Sass variables?
Sass variables (declared with $variable: value) are resolved at compile time — they become static values in the output CSS and cannot change at runtime. CSS custom properties (--variable: value) are resolved at runtime by the browser, support the cascade, can be scoped to elements, can be read and written by JavaScript, and can be animated. For design systems, CSS custom properties are strictly more powerful; Sass variables are useful for build-time constants and mixins.
How do I implement a three-layer design token architecture?
The standard approach uses three layers: Base tokens define raw values (--blue-500: #3b82f6; --space-4: 16px). Semantic tokens reference base tokens with context-meaningful names (--color-interactive: var(--blue-500); --spacing-component: var(--space-4)). Component tokens reference semantic tokens for specific components (--button-bg: var(--color-interactive); --button-padding: var(--spacing-component)). This hierarchy ensures a single base color change propagates correctly through all semantic and component tokens.
How do I sync Figma variables with CSS custom properties?
The Tokens Studio for Figma plugin (formerly Figma Tokens) exports Figma variables as JSON design token files that can be converted to CSS custom property declarations via the Style Dictionary tool. The workflow: define variables in Figma → export via Tokens Studio → process with Style Dictionary → generate a CSS file of custom property declarations that your design system imports. This keeps Figma and code in sync without manual copy-pasting of values.
Can CSS custom properties be animated?
CSS custom properties can be transitioned and animated if the browser can interpolate their values. As of 2026, @property allows you to define the type and initial value of a custom property, enabling full CSS animation: @property --hue { syntax: '<angle>'; inherits: false; initial-value: 0deg; } .element { animation: spin 3s linear infinite; } @keyframes spin { to { --hue: 360deg; } }. Without @property, custom properties are interpolated as discrete values (no smooth transition).
How do I inspect CSS custom property values on a live page?
In Chrome DevTools, select an element in the Elements panel and look in the Computed tab for custom property values. In onHover, the CSS inspector shows computed values including resolved custom property values for any element on hover. The Source tab in onHover's inspector shows which stylesheet defines each custom property, making it easy to trace token values back to their origin file.