Implement a complete design token system that bridges Figma Variables and CSS custom properties — primitive and semantic token layers, dark mode support, token export pipelines, and CSS-in-JS integration.
Design tokens are the shared vocabulary between design and code. When a designer updates the 'Primary' button color in Figma Variables and a developer updates --color-action in CSS, both should be changing the same value from the same source of truth.
Implementing design tokens correctly means building a two-layer system: primitive tokens (the raw palette) that never change and semantic tokens (purpose-based aliases) that designers and developers both update when the design evolves. Dark mode, theming, and design system updates all become single-point changes.
This lesson builds a complete token system from Figma Variables export to production CSS, including automation with Style Dictionary.
A design token system has two layers. Getting this distinction right is the foundation of a maintainable system.
Primitive tokens: raw values
Semantic tokens: purpose-based aliases
Why this matters:
1/* Without semantic tokens: change brand color = update 200+ places */
2.btn-primary { background-color: #2563eb; } /* what is this? */
3.nav-link { color: #2563eb; } /* action? accent? primary? */
4.badge-featured { background-color: #2563eb; }
5
6/* With semantic tokens: change brand = update ONE line */
7:root { --color-action: #2563eb; } /* all components reference --color-action */
8.btn-primary { background-color: var(--color-action); }
9.nav-link { color: var(--color-action); }
10.badge-featured { background-color: var(--color-action); }
11/* Change brand: :root { --color-action: #7c3aed; } — done */Full two-layer CSS implementation:
1/* Layer 1: Primitive tokens (the complete palette) */
2:root {
3 /* Blue scale */
4 --blue-50: #eff6ff;
5 --blue-100: #dbeafe;
6 --blue-200: #bfdbfe;
7 --blue-300: #93c5fd;
8 --blue-400: #60a5fa;
9 --blue-500: #3b82f6;
10 --blue-600: #2563eb;
11 --blue-700: #1d4ed8;
12 --blue-800: #1e40af;
13 --blue-900: #1e3a8a;
14
15 /* Green, Red, Amber, Purple scales */
16 --green-600: #16a34a;
17 --red-600: #dc2626;
18 --amber-500: #f59e0b;
19 --purple-600: #9333ea;
20
21 /* Neutral scale */
22 --gray-0: #ffffff;
23 --gray-50: #f9fafb;
24 --gray-100: #f3f4f6;
25 --gray-200: #e5e7eb;
26 --gray-300: #d1d5db;
27 --gray-400: #9ca3af;
28 --gray-500: #6b7280;
29 --gray-600: #4b5563;
30 --gray-700: #374151;
31 --gray-800: #1f2937;
32 --gray-900: #111827;
33
34 /* Spacing scale (4px base) */
35 --space-1: 0.25rem; /* 4px */
36 --space-2: 0.5rem; /* 8px */
37 --space-3: 0.75rem; /* 12px */
38 --space-4: 1rem; /* 16px */
39 --space-6: 1.5rem; /* 24px */
40 --space-8: 2rem; /* 32px */
41 --space-10: 2.5rem; /* 40px */
42 --space-12: 3rem; /* 48px */
43 --space-16: 4rem; /* 64px */
44
45 /* Border radius */
46 --radius-sm: 0.25rem;
47 --radius-md: 0.5rem;
48 --radius-lg: 0.75rem;
49 --radius-xl: 1rem;
50 --radius-full: 9999px;
51}
52
53/* Layer 2: Semantic tokens (light mode — default) */
54:root {
55 --color-bg-page: var(--gray-0);
56 --color-bg-surface: var(--gray-50);
57 --color-bg-elevated: var(--gray-0);
58 --color-text: var(--gray-900);
59 --color-text-muted: var(--gray-500);
60 --color-border: var(--gray-200);
61 --color-border-strong: var(--gray-300);
62 --color-action: var(--blue-600);
63 --color-action-hover: var(--blue-700);
64 --color-success: var(--green-600);
65 --color-danger: var(--red-600);
66 --color-warning: var(--amber-500);
67 --color-info: var(--blue-600);
68}Semantic tokens make dark mode implementation trivial — only the semantic layer changes, never the primitive layer or component styles.
Dark mode semantic token overrides:
1/* Dark mode: reassign semantic tokens to different primitives */
2@media (prefers-color-scheme: dark) {
3 :root {
4 --color-bg-page: var(--gray-900); /* was gray-0 */
5 --color-bg-surface: var(--gray-800); /* was gray-50 */
6 --color-bg-elevated: var(--gray-700);
7 --color-text: var(--gray-50); /* was gray-900 */
8 --color-text-muted: var(--gray-400); /* was gray-500 */
9 --color-border: var(--gray-700); /* was gray-200 */
10 --color-border-strong: var(--gray-600);
11 --color-action: var(--blue-400); /* lighter for dark bg */
12 --color-action-hover: var(--blue-300);
13 /* Success, danger, warning: often stay similar in dark mode */
14 --color-success: var(--green-400, #4ade80);
15 --color-danger: var(--red-400, #f87171);
16 }
17}
18
19/* Class-based dark mode (if JavaScript toggles it) */
20.dark {
21 --color-bg-page: var(--gray-900);
22 --color-text: var(--gray-50);
23 /* ... all semantic overrides */
24}Components using semantic tokens automatically switch modes:
1/* This component needs ZERO changes for dark mode */
2.card {
3 background-color: var(--color-bg-elevated); /* white in light, dark-700 in dark */
4 border: 1px solid var(--color-border); /* gray-200 in light, gray-700 in dark */
5 color: var(--color-text); /* gray-900 in light, gray-50 in dark */
6}
7
8.card__title {
9 color: var(--color-text); /* automatically inverts */
10}
11
12.card__meta {
13 color: var(--color-text-muted); /* automatically adjusts */
14}Testing token dark mode:
1// In browser DevTools console — temporarily force dark mode:
2window.matchMedia('(prefers-color-scheme: dark)').matches
3// true if system is in dark mode
4
5// DevTools shortcut for testing:
6// F12 → Rendering tab → Emulate CSS media feature
7// → prefers-color-scheme: dark
8// Instantly applies dark mode without changing system settingsManual token management (copying hex values from Figma) does not scale. Tokens Studio for Figma exports variables to JSON, and Style Dictionary transforms that JSON to any output format.
Tokens Studio setup:
Tokens Studio JSON format:
1{
2 "color": {
3 "primitive": {
4 "blue": {
5 "600": { "value": "#2563eb", "type": "color" },
6 "700": { "value": "#1d4ed8", "type": "color" }
7 },
8 "gray": {
9 "900": { "value": "#111827", "type": "color" },
10 "50": { "value": "#f9fafb", "type": "color" }
11 }
12 },
13 "semantic": {
14 "action": { "value": "{color.primitive.blue.600}", "type": "color" },
15 "text": { "value": "{color.primitive.gray.900}", "type": "color" },
16 "bg-page": { "value": "{color.primitive.gray.50}", "type": "color" }
17 }
18 },
19 "spacing": {
20 "4": { "value": "16px", "type": "spacing" },
21 "6": { "value": "24px", "type": "spacing" }
22 }
23}Style Dictionary setup:
1npm install -D style-dictionary1// style-dictionary.config.js
2const StyleDictionary = require('style-dictionary');
3
4module.exports = {
5 source: ['tokens/**/*.json'],
6 platforms: {
7 css: {
8 transformGroup: 'css',
9 buildPath: 'src/styles/',
10 files: [
11 {
12 destination: 'primitives.css',
13 format: 'css/variables',
14 filter: (token) => token.path[1] === 'primitive',
15 options: { selector: ':root' }
16 },
17 {
18 destination: 'tokens.css',
19 format: 'css/variables',
20 filter: (token) => token.path[1] === 'semantic',
21 options: { selector: ':root', outputReferences: true }
22 }
23 ]
24 }
25 }
26};1# Build tokens
2npx style-dictionary build
3
4# Generated output:
5# src/styles/primitives.css → :root { --color-primitive-blue-600: #2563eb; ... }
6# src/styles/tokens.css → :root { --color-semantic-action: var(--color-primitive-blue-600); }Automated token sync in CI:
1# .github/workflows/tokens.yml
2on:
3 push:
4 paths: ['tokens/**']
5
6jobs:
7 build-tokens:
8 steps:
9 - run: npx style-dictionary build
10 - run: git add src/styles/
11 - run: git commit -m 'chore(tokens): regenerate CSS from Figma export'Tailwind CSS v4 uses CSS custom properties natively — your design tokens become Tailwind theme values automatically when configured correctly.
Tailwind v4 configuration (CSS-first approach):
1/* src/styles/tailwind.css */
2@import 'tailwindcss';
3
4/* Reference your existing CSS tokens */
5@theme {
6 /* Map CSS custom properties to Tailwind theme */
7 --color-action: var(--color-action); /* Tailwind: text-action, bg-action */
8 --color-success: var(--color-success);
9 --color-danger: var(--color-danger);
10
11 /* Custom spacing extending Tailwind defaults */
12 --spacing-4-5: var(--space-4-5, 1.125rem); /* non-default size */
13
14 /* Custom border radius */
15 --radius-lg: var(--radius-lg);
16}Using tokens in Tailwind classes:
1<!-- Using token-derived classes -->
2<button class="bg-action hover:bg-action-hover text-white rounded-lg">
3 Enroll now
4</button>
5
6<p class="text-text-muted">
7 Muted text that switches automatically in dark mode
8</p>Fallback: when NOT using Tailwind, use the tokens directly in CSS:
1/* Any component — works with or without Tailwind */
2.btn-primary {
3 background-color: var(--color-action);
4 color: var(--gray-0);
5 padding: var(--space-3) var(--space-6);
6 border-radius: var(--radius-md);
7}
8
9.btn-primary:hover {
10 background-color: var(--color-action-hover);
11}Design token update flow (end-to-end):
11. Designer updates color in Figma Variables
22. Tokens Studio pushes change to tokens/colors.json in GitHub
33. CI/CD detects change → runs style-dictionary build
44. Generated CSS updated: --color-action: #7c3aed (was #2563eb)
55. PR created with CSS diff for team review
66. After merge: all components using var(--color-action) update
77. No component-level CSS changes neededImplementing the full two-layer CSS token system that powers all components, with dark mode and Tailwind integration.
1/* src/styles/tokens.css — Complete Design Token System */
2
3/* ============================================
4 LAYER 1: PRIMITIVE TOKENS
5 Source: Figma Variables > Primitives collection
6 Rule: Never use these directly in components
7 ============================================ */
8
9:root {
10 /* Blue palette */
11 --prim-blue-50: #eff6ff;
12 --prim-blue-100: #dbeafe;
13 --prim-blue-400: #60a5fa;
14 --prim-blue-500: #3b82f6;
15 --prim-blue-600: #2563eb;
16 --prim-blue-700: #1d4ed8;
17 --prim-blue-800: #1e40af;
18
19 /* Green, Red, Amber */
20 --prim-green-400: #4ade80;
21 --prim-green-600: #16a34a;
22 --prim-red-400: #f87171;
23 --prim-red-600: #dc2626;
24 --prim-amber-400: #fbbf24;
25 --prim-amber-500: #f59e0b;
26 --prim-purple-600: #9333ea;
27
28 /* Neutral */
29 --prim-gray-0: #ffffff;
30 --prim-gray-50: #f9fafb;
31 --prim-gray-100: #f3f4f6;
32 --prim-gray-200: #e5e7eb;
33 --prim-gray-300: #d1d5db;
34 --prim-gray-400: #9ca3af;
35 --prim-gray-500: #6b7280;
36 --prim-gray-600: #4b5563;
37 --prim-gray-700: #374151;
38 --prim-gray-800: #1f2937;
39 --prim-gray-900: #111827;
40
41 /* Spacing (4px base) */
42 --prim-sp-1: 0.25rem; /* 4px */
43 --prim-sp-2: 0.5rem; /* 8px */
44 --prim-sp-3: 0.75rem; /* 12px */
45 --prim-sp-4: 1rem; /* 16px */
46 --prim-sp-5: 1.25rem; /* 20px */
47 --prim-sp-6: 1.5rem; /* 24px */
48 --prim-sp-8: 2rem; /* 32px */
49 --prim-sp-10: 2.5rem; /* 40px */
50 --prim-sp-12: 3rem; /* 48px */
51 --prim-sp-16: 4rem; /* 64px */
52
53 /* Radius */
54 --prim-r-sm: 0.25rem;
55 --prim-r-md: 0.5rem;
56 --prim-r-lg: 0.75rem;
57 --prim-r-xl: 1rem;
58 --prim-r-2xl: 1.5rem;
59 --prim-r-full: 9999px;
60}
61
62/* ============================================
63 LAYER 2: SEMANTIC TOKENS (LIGHT MODE)
64 Source: Figma Variables > Semantic collection
65 Rule: All components use ONLY these
66 ============================================ */
67
68:root {
69 /* Background */
70 --color-bg-page: var(--prim-gray-0);
71 --color-bg-surface: var(--prim-gray-50);
72 --color-bg-elevated: var(--prim-gray-0);
73 --color-bg-overlay: rgb(0 0 0 / 0.5);
74
75 /* Text */
76 --color-text-primary: var(--prim-gray-900);
77 --color-text-muted: var(--prim-gray-500);
78 --color-text-disabled: var(--prim-gray-400);
79 --color-text-inverse: var(--prim-gray-0);
80
81 /* Border */
82 --color-border: var(--prim-gray-200);
83 --color-border-strong: var(--prim-gray-300);
84 --color-border-focus: var(--prim-blue-600);
85
86 /* Action (primary interactive elements) */
87 --color-action: var(--prim-blue-600);
88 --color-action-hover: var(--prim-blue-700);
89 --color-action-subtle: var(--prim-blue-50);
90
91 /* Status */
92 --color-success: var(--prim-green-600);
93 --color-danger: var(--prim-red-600);
94 --color-warning: var(--prim-amber-500);
95 --color-info: var(--prim-blue-600);
96
97 /* Spacing shortcuts */
98 --space-component-padding-x: var(--prim-sp-4); /* 16px default */
99 --space-component-padding-y: var(--prim-sp-3); /* 12px default */
100 --space-section-gap: var(--prim-sp-12); /* 48px between sections */
101
102 /* Typography */
103 --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
104 --font-mono: 'JetBrains Mono', 'Fira Code', monospace;
105}
106
107/* ============================================
108 DARK MODE: Reassign semantic tokens only
109 Primitives stay identical — only references change
110 ============================================ */
111
112@media (prefers-color-scheme: dark) {
113 :root {
114 --color-bg-page: var(--prim-gray-900);
115 --color-bg-surface: var(--prim-gray-800);
116 --color-bg-elevated: var(--prim-gray-700);
117 --color-text-primary: var(--prim-gray-50);
118 --color-text-muted: var(--prim-gray-400);
119 --color-text-disabled: var(--prim-gray-600);
120 --color-border: var(--prim-gray-700);
121 --color-border-strong: var(--prim-gray-600);
122 --color-action: var(--prim-blue-400);
123 --color-action-hover: var(--prim-blue-300);
124 --color-action-subtle: var(--prim-blue-900, #1e3a8a);
125 --color-success: var(--prim-green-400);
126 --color-danger: var(--prim-red-400);
127 }
128}
129
130/* JavaScript dark mode class (alternative to media query) */
131[data-theme='dark'] {
132 --color-bg-page: var(--prim-gray-900);
133 --color-text-primary: var(--prim-gray-50);
134 /* ... same as @media block above */
135}The --prim- prefix on primitive tokens makes it visually clear they should never be used in components — if you see --prim- in a component's CSS, it is a design token violation. All component CSS uses --color- semantic tokens. The dark mode block only reassigns semantic tokens — not a single primitive token changes, and not a single component rule needs modification. The data-theme='dark' alternative enables JavaScript-controlled dark mode (user preference toggle) alongside the automatic system preference version.
The next lesson covers Version Control for Design Files — how to manage Figma file history, create named versions for developer handoff milestones, and use git-adjacent workflows to track design changes over time.
Your progress is saved in this browser, on this device only.