Design tokens are the single most impactful structural decision in any design system I have worked on. They are not a trend or a Figma feature. They are the contractual layer between design and code. When done right, a color change in Figma propagates to production in minutes. When done wrong, you spend weeks hunting down hardcoded hex values. This post walks through the entire workflow I use: from Figma Variables to CSS custom properties to Angular components, including theming.
What Design Tokens Actually Are
A design token is a named value that represents a design decision. Instead of saying 'this button is #0052CC', you say 'this button uses the color token brand-primary.' The token has a name, a value, and a category. The name is stable. The value can change. This separation is what makes theming and rebranding tractable.
There are typically three layers of tokens. Primitive tokens (also called global or reference tokens) are raw values: every color in your palette, every spacing step, every font size. Semantic tokens (also called alias or decision tokens) reference primitives and carry design intent: color-button-primary-bg, spacing-section-gap, text-heading-large. Component tokens are the most specific and reference semantic tokens: button-primary-background, card-padding.
- Primitive: color-blue-600 = #0052CC
- Semantic: color-action-primary = color-blue-600
- Component: button-primary-bg = color-action-primary
This three-layer model sounds like overhead. It is not. It is what lets you rebrand by changing one semantic token mapping instead of hunting through hundreds of components. If the primary action color changes, the edit happens in one place, the semantic mapping, and every component that consumes color-action-primary picks it up without being touched.
Defining Tokens in Figma Variables
Figma's Variables feature (released in 2023 and significantly improved since) is now the right place to define design tokens. Before Variables, designers used plugins like Tokens Studio. Variables is native, syncs in real time, and has direct support for modes, which is how theming works in Figma.
My Figma structure uses collections organized by token layer. I create three collections: Primitives, Semantic, and Component. Each collection can have modes. The Primitives collection typically has just one mode. Values do not change across themes. The Semantic collection has modes for Light and Dark. The Component collection inherits from Semantic.
- Create a 'Primitives' collection. All raw color, spacing, radius, typography values
- Create a 'Semantic' collection with Light and Dark modes. Values reference Primitives
- Create a 'Component' collection. Values reference Semantic tokens
- Name tokens using kebab-case to match CSS custom property conventions
- Group tokens using slash notation: color/action/primary. Figma renders this as a nested hierarchy
- Document every semantic token with a description: what it is for, what it should never be used for
The naming discipline is critical. I have seen systems where tokens are named after their values (blue-500, 16px) rather than their purpose (action-primary, spacing-base). When the value changes, the name becomes a lie. Name tokens for what they mean, not what they currently are.
Exporting Tokens to CSS Custom Properties
The export pipeline I use goes from Figma Variables to a JSON tokens file to CSS custom properties. The variables are exported from Figma as JSON, the JSON is committed to the repository, and Style Dictionary does the transformation. The export step matters less than people think: any plugin or script that writes the variables out as JSON will do. What matters is that the JSON lands in git and nobody edits the generated CSS by hand.
The step most write-ups skip is how a Figma mode becomes a file on disk. A variable in the Semantic collection does not have one value, it has one value per mode, so a single flat export of that collection is ambiguous. Export once per mode and give each export its own directory: tokens/primitives/ for the Primitives collection, tokens/semantic-light/ and tokens/semantic-dark/ for the two modes of Semantic. The token names are identical across the two theme directories and only the values differ. That is what lets the build produce one CSS file per theme.
On the export tool itself, start free. Community plugins like variables2json or Export Variables to JSON let you pick a collection and a mode and save the result as JSON, which is everything the pipeline needs. Figma's Variables REST API is the cleaner way to automate the same export, but it is gated to the Enterprise plan. Move to it only if you are already on Enterprise and want the export running in CI instead of in a designer's browser.
Style Dictionary is an open-source tool from Amazon that transforms token files into any platform format: CSS custom properties, SCSS variables, iOS Swift constants, Android XML. For web projects I configure it to output CSS custom properties organized by theme. Check your major version before you copy any config. Everything below is v3 and its CommonJS API. v4 changed the entry point to a class and removed token.attributes.category in favour of token.type, so v3 filters do not carry over.
// build-tokens.js (Style Dictionary v3, CommonJS API)
const StyleDictionary = require('style-dictionary');
const cssPlatform = (destination, selector, outputReferences, filter) => ({
css: {
transformGroup: 'css',
prefix: 'ds',
buildPath: 'src/styles/tokens/',
files: [
{ destination, format: 'css/variables', filter, options: { selector, outputReferences } }
]
}
});
// One build per output file, each with its own source glob.
// Do not try to split a single build with filters on token.attributes:
// transformGroup 'css' runs attribute/cti, which overwrites attributes with
// category/type/item derived from the token path. A test like
// attributes.category === 'primitive' never matches and the file builds empty
// with no error.
const isSemantic = (token) => token.filePath.includes('/semantic-');
StyleDictionary.extend({
source: ['tokens/primitives/**/*.json'],
platforms: cssPlatform('primitives.css', ':root', false)
}).buildAllPlatforms();
// include loads the primitives so the var() references resolve.
// isSemantic keeps them out of the theme file.
StyleDictionary.extend({
include: ['tokens/primitives/**/*.json'],
source: ['tokens/semantic-light/**/*.json'],
platforms: cssPlatform('semantic-light.css', ':root, [data-theme="light"]', true, isSemantic)
}).buildAllPlatforms();
StyleDictionary.extend({
include: ['tokens/primitives/**/*.json'],
source: ['tokens/semantic-dark/**/*.json'],
platforms: cssPlatform('semantic-dark.css', '[data-theme="dark"]', true, isSemantic)
}).buildAllPlatforms();The outputReferences: true option is important for semantic tokens. Instead of resolving the final hex value at build time, it outputs var(--ds-color-blue-600), meaning the CSS retains the reference chain. This allows runtime theming without rebuilding the token files.
The Generated CSS
After running Style Dictionary, you get CSS files that look like this. Primitives define raw values. Semantic tokens reference primitives. Theme switching becomes a data attribute on the html element.
/* primitives.css */
:root {
--ds-color-blue-100: #EBF2FF;
--ds-color-blue-200: #BDD5FF;
--ds-color-blue-600: #0052CC;
--ds-color-blue-700: #0043A6;
--ds-color-neutral-0: #FFFFFF;
--ds-color-neutral-900: #111827;
--ds-spacing-1: 4px;
--ds-spacing-2: 8px;
--ds-spacing-4: 16px;
--ds-spacing-6: 24px;
--ds-spacing-8: 32px;
--ds-radius-sm: 4px;
--ds-radius-md: 8px;
--ds-radius-lg: 16px;
}
/* semantic-light.css */
:root, [data-theme="light"] {
--ds-color-action-primary: var(--ds-color-blue-600);
--ds-color-action-primary-hover: var(--ds-color-blue-700);
--ds-color-surface-default: var(--ds-color-neutral-0);
--ds-color-text-primary: var(--ds-color-neutral-900);
--ds-spacing-section: var(--ds-spacing-8);
--ds-spacing-component: var(--ds-spacing-4);
}
/* semantic-dark.css */
[data-theme="dark"] {
--ds-color-action-primary: var(--ds-color-blue-200);
--ds-color-action-primary-hover: var(--ds-color-blue-100);
--ds-color-surface-default: var(--ds-color-neutral-900);
--ds-color-text-primary: var(--ds-color-neutral-0);
}Consuming Tokens in Angular Components
The CSS files are imported once in the Angular project's global styles. Individual components never import tokens directly. They consume them through the cascade. This is the correct pattern. If a component imports a token file directly, you lose the ability to override tokens via themes.
/* styles.scss, global entry point */
@use 'styles/tokens/primitives.css';
@use 'styles/tokens/semantic-light.css';
@use 'styles/tokens/semantic-dark.css';
/* Component styles, no direct token imports */
/* src/app/components/button/button.component.scss */
.btn-primary {
background-color: var(--ds-color-action-primary);
color: var(--ds-color-neutral-0);
padding: var(--ds-spacing-2) var(--ds-spacing-4);
border-radius: var(--ds-radius-md);
border: none;
cursor: pointer;
transition: background-color 200ms ease;
&:hover {
background-color: var(--ds-color-action-primary-hover);
}
&:focus-visible {
outline: 2px solid var(--ds-color-action-primary);
outline-offset: 2px;
}
}Theme Switching at Runtime
Because semantic tokens reference primitives via var() chains that the browser resolves at computed-value time, theme switching requires only setting a data-theme attribute on the document element. No re-render, no class toggle gymnastics, no JavaScript style mutations.
// theme.service.ts
import { Injectable, Renderer2, RendererFactory2 } from '@angular/core';
export type Theme = 'light' | 'dark';
@Injectable({ providedIn: 'root' })
export class ThemeService {
private renderer: Renderer2;
private currentTheme: Theme = 'light';
constructor(rendererFactory: RendererFactory2) {
this.renderer = rendererFactory.createRenderer(null, null);
this.initTheme();
}
private initTheme(): void {
const stored = localStorage.getItem('theme') as Theme;
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = stored ?? (prefersDark ? 'dark' : 'light');
this.setTheme(theme);
}
setTheme(theme: Theme): void {
this.currentTheme = theme;
this.renderer.setAttribute(
document.documentElement,
'data-theme',
theme
);
localStorage.setItem('theme', theme);
}
getTheme(): Theme {
return this.currentTheme;
}
toggle(): void {
this.setTheme(this.currentTheme === 'light' ? 'dark' : 'light');
}
}Component-Level Token Overrides
One of the most powerful aspects of CSS custom properties is that they cascade. You can override them at any scope. This enables component-level theming without duplicating component styles. An inverted card, a promotional banner, or a sidebar with a different color scheme can override just the tokens it needs.
/* Inverted surface, override tokens at component scope */
.surface-inverted {
--ds-color-action-primary: var(--ds-color-blue-200);
--ds-color-text-primary: var(--ds-color-neutral-0);
--ds-color-surface-default: var(--ds-color-blue-700);
background-color: var(--ds-color-surface-default);
color: var(--ds-color-text-primary);
/* All child components automatically use overridden tokens */
/* No component code changes required */
}Keeping Figma and Code in Sync
The most common failure mode in design token systems is drift. Figma gets updated but the code does not, or vice versa. I address this with a CI pipeline step that runs Style Dictionary on every merge to main and fails the build if the output differs from committed token files. This forces a deliberate commit whenever token values change.
- Export tokens from Figma on a scheduled basis (weekly or per release) and commit the JSON source
- Run Style Dictionary in CI. Fail the build if generated CSS differs from committed CSS
- Use semantic versioning for the token package if shared across multiple apps
- Document breaking changes in a CHANGELOG. A token rename is a breaking change for every consumer
- Never let individual developers hardcode hex values in component CSS. Enforce through linting
- Consider a Stylelint rule that disallows raw color values and enforces var(--ds-) references
The Payoff
The investment in a token system pays back quickly. In my last months at Wonderlabs, in 2023, I put the design system foundation I had built for the credit card PWA on this pipeline. It could not have been earlier: Figma only shipped Variables in June 2023. The flow is the one in this post: Figma Variables exported as JSON, Style Dictionary turning that JSON into CSS custom properties, Angular components consuming them through the cascade. The payoff is a property of the structure, not of any one team: components built on semantic tokens are themeable by default, and trying a color adjustment means changing one token and re-running the build instead of editing component styles.
Design tokens are not about Figma and CSS agreeing on a hex value. They are about creating a shared language between design and engineering that survives personnel changes, rebrands, and platform evolution.
Start with primitives. Name them for meaning, not value. Build semantic tokens that capture design intent. Export to CSS custom properties. Let the cascade do the theming work. The same structure works for a single app and for a component library shared by several branded apps. Only the number of consumers changes.
References
- Style Dictionary. https://styledictionary.com/
- Tokens Studio. https://tokens.studio/
- Figma Variables REST API (Enterprise plan only). https://developers.figma.com/docs/rest-api/variables/
- MDN CSS custom properties. https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties
- Design Tokens Community Group (W3C Community Group). Design Tokens Format Module 2025.10. https://www.designtokens.org/tr/2025.10/format/