Back to Blog
Design TokensDesign SystemsCSSTailwind

Design Tokens: Building Consistent Design Systems

Learn how to implement design tokens for scalable, consistent UI development. From tokens to components across platforms.

B
Bootspring Team
Engineering
February 26, 2026
7 min read

Design tokens are the atomic values of your design system—colors, typography, spacing, and more. This guide shows how to implement and scale design tokens effectively.

What Are Design Tokens?#

Design tokens are named entities storing visual design attributes:

1{ 2 "color": { 3 "primary": { 4 "value": "#2563eb", 5 "type": "color" 6 }, 7 "text": { 8 "default": { "value": "#1f2937", "type": "color" }, 9 "muted": { "value": "#6b7280", "type": "color" } 10 } 11 }, 12 "spacing": { 13 "xs": { "value": "4px", "type": "dimension" }, 14 "sm": { "value": "8px", "type": "dimension" }, 15 "md": { "value": "16px", "type": "dimension" }, 16 "lg": { "value": "24px", "type": "dimension" }, 17 "xl": { "value": "32px", "type": "dimension" } 18 } 19}

Token Architecture#

Three-Tier Token System#

┌─────────────────────────────────────────────────────────┐ │ SEMANTIC TOKENS (Component-level) │ │ button-primary-background → color-brand-500 │ │ card-shadow → shadow-md │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ ALIAS TOKENS (Theme-level) │ │ color-brand-500 → color-blue-500 │ │ color-text-primary → color-gray-900 │ └─────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────┐ │ PRIMITIVE TOKENS (Raw values) │ │ color-blue-500 → #3b82f6 │ │ spacing-4 → 16px │ └─────────────────────────────────────────────────────────┘

Token File Structure#

tokens/ ├── primitives/ │ ├── colors.json │ ├── typography.json │ ├── spacing.json │ └── shadows.json ├── semantic/ │ ├── light.json │ └── dark.json ├── components/ │ ├── button.json │ ├── input.json │ └── card.json └── index.js

Defining Tokens#

Color Tokens#

1// tokens/primitives/colors.json 2{ 3 "color": { 4 "blue": { 5 "50": { "value": "#eff6ff" }, 6 "100": { "value": "#dbeafe" }, 7 "200": { "value": "#bfdbfe" }, 8 "300": { "value": "#93c5fd" }, 9 "400": { "value": "#60a5fa" }, 10 "500": { "value": "#3b82f6" }, 11 "600": { "value": "#2563eb" }, 12 "700": { "value": "#1d4ed8" }, 13 "800": { "value": "#1e40af" }, 14 "900": { "value": "#1e3a8a" } 15 }, 16 "gray": { 17 "50": { "value": "#f9fafb" }, 18 "100": { "value": "#f3f4f6" }, 19 "500": { "value": "#6b7280" }, 20 "900": { "value": "#111827" } 21 } 22 } 23}

Semantic Tokens with References#

1// tokens/semantic/light.json 2{ 3 "color": { 4 "background": { 5 "default": { "value": "{color.white}" }, 6 "subtle": { "value": "{color.gray.50}" }, 7 "muted": { "value": "{color.gray.100}" } 8 }, 9 "text": { 10 "default": { "value": "{color.gray.900}" }, 11 "muted": { "value": "{color.gray.500}" }, 12 "inverse": { "value": "{color.white}" } 13 }, 14 "brand": { 15 "default": { "value": "{color.blue.600}" }, 16 "hover": { "value": "{color.blue.700}" }, 17 "active": { "value": "{color.blue.800}" } 18 } 19 } 20}
1// tokens/semantic/dark.json 2{ 3 "color": { 4 "background": { 5 "default": { "value": "{color.gray.900}" }, 6 "subtle": { "value": "{color.gray.800}" }, 7 "muted": { "value": "{color.gray.700}" } 8 }, 9 "text": { 10 "default": { "value": "{color.gray.50}" }, 11 "muted": { "value": "{color.gray.400}" }, 12 "inverse": { "value": "{color.gray.900}" } 13 }, 14 "brand": { 15 "default": { "value": "{color.blue.500}" }, 16 "hover": { "value": "{color.blue.400}" }, 17 "active": { "value": "{color.blue.300}" } 18 } 19 } 20}

Typography Tokens#

1// tokens/primitives/typography.json 2{ 3 "font": { 4 "family": { 5 "sans": { "value": "Inter, system-ui, sans-serif" }, 6 "mono": { "value": "JetBrains Mono, monospace" } 7 }, 8 "size": { 9 "xs": { "value": "0.75rem" }, 10 "sm": { "value": "0.875rem" }, 11 "base": { "value": "1rem" }, 12 "lg": { "value": "1.125rem" }, 13 "xl": { "value": "1.25rem" }, 14 "2xl": { "value": "1.5rem" }, 15 "3xl": { "value": "1.875rem" }, 16 "4xl": { "value": "2.25rem" } 17 }, 18 "weight": { 19 "normal": { "value": "400" }, 20 "medium": { "value": "500" }, 21 "semibold": { "value": "600" }, 22 "bold": { "value": "700" } 23 }, 24 "lineHeight": { 25 "tight": { "value": "1.25" }, 26 "normal": { "value": "1.5" }, 27 "relaxed": { "value": "1.75" } 28 } 29 } 30}

Component Tokens#

1// tokens/components/button.json 2{ 3 "button": { 4 "primary": { 5 "background": { "value": "{color.brand.default}" }, 6 "backgroundHover": { "value": "{color.brand.hover}" }, 7 "backgroundActive": { "value": "{color.brand.active}" }, 8 "text": { "value": "{color.text.inverse}" }, 9 "borderRadius": { "value": "{radius.md}" } 10 }, 11 "secondary": { 12 "background": { "value": "transparent" }, 13 "backgroundHover": { "value": "{color.background.subtle}" }, 14 "text": { "value": "{color.text.default}" }, 15 "border": { "value": "{color.border.default}" } 16 }, 17 "padding": { 18 "sm": { "value": "{spacing.2} {spacing.3}" }, 19 "md": { "value": "{spacing.2} {spacing.4}" }, 20 "lg": { "value": "{spacing.3} {spacing.6}" } 21 } 22 } 23}

Transforming Tokens#

Using Style Dictionary#

1// 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: 'dist/css/', 10 files: [ 11 { 12 destination: 'variables.css', 13 format: 'css/variables', 14 options: { 15 outputReferences: true, 16 }, 17 }, 18 ], 19 }, 20 js: { 21 transformGroup: 'js', 22 buildPath: 'dist/js/', 23 files: [ 24 { 25 destination: 'tokens.js', 26 format: 'javascript/es6', 27 }, 28 ], 29 }, 30 scss: { 31 transformGroup: 'scss', 32 buildPath: 'dist/scss/', 33 files: [ 34 { 35 destination: '_variables.scss', 36 format: 'scss/variables', 37 }, 38 ], 39 }, 40 }, 41};

Generated CSS Output#

1/* dist/css/variables.css */ 2:root { 3 /* Primitives */ 4 --color-blue-500: #3b82f6; 5 --color-blue-600: #2563eb; 6 --color-gray-900: #111827; 7 8 /* Semantic */ 9 --color-brand-default: var(--color-blue-600); 10 --color-text-default: var(--color-gray-900); 11 12 /* Components */ 13 --button-primary-background: var(--color-brand-default); 14 --button-primary-text: var(--color-text-inverse); 15} 16 17[data-theme="dark"] { 18 --color-brand-default: var(--color-blue-500); 19 --color-text-default: var(--color-gray-50); 20}

Integration with Tailwind#

Extending Tailwind Config#

1// tailwind.config.js 2const tokens = require('./dist/js/tokens'); 3 4module.exports = { 5 theme: { 6 extend: { 7 colors: { 8 brand: { 9 DEFAULT: 'var(--color-brand-default)', 10 hover: 'var(--color-brand-hover)', 11 active: 'var(--color-brand-active)', 12 }, 13 background: { 14 DEFAULT: 'var(--color-background-default)', 15 subtle: 'var(--color-background-subtle)', 16 muted: 'var(--color-background-muted)', 17 }, 18 }, 19 spacing: tokens.spacing, 20 fontSize: tokens.fontSize, 21 fontFamily: { 22 sans: tokens.fontFamily.sans, 23 mono: tokens.fontFamily.mono, 24 }, 25 }, 26 }, 27};

Using in Components#

1// Button.jsx 2function Button({ variant = 'primary', size = 'md', children }) { 3 const baseClasses = 'inline-flex items-center justify-center font-medium rounded-md transition-colors'; 4 5 const variants = { 6 primary: 'bg-brand text-white hover:bg-brand-hover active:bg-brand-active', 7 secondary: 'bg-transparent border border-border-default text-text-default hover:bg-background-subtle', 8 }; 9 10 const sizes = { 11 sm: 'px-3 py-1.5 text-sm', 12 md: 'px-4 py-2 text-base', 13 lg: 'px-6 py-3 text-lg', 14 }; 15 16 return ( 17 <button className={`${baseClasses} ${variants[variant]} ${sizes[size]}`}> 18 {children} 19 </button> 20 ); 21}

Theme Switching#

CSS Variables Approach#

1// ThemeProvider.jsx 2import { createContext, useContext, useEffect, useState } from 'react'; 3 4const ThemeContext = createContext(); 5 6export function ThemeProvider({ children }) { 7 const [theme, setTheme] = useState('light'); 8 9 useEffect(() => { 10 document.documentElement.setAttribute('data-theme', theme); 11 }, [theme]); 12 13 return ( 14 <ThemeContext.Provider value={{ theme, setTheme }}> 15 {children} 16 </ThemeContext.Provider> 17 ); 18} 19 20export const useTheme = () => useContext(ThemeContext);

System Preference Detection#

1// hooks/useSystemTheme.js 2import { useEffect, useState } from 'react'; 3 4export function useSystemTheme() { 5 const [systemTheme, setSystemTheme] = useState('light'); 6 7 useEffect(() => { 8 const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 9 setSystemTheme(mediaQuery.matches ? 'dark' : 'light'); 10 11 const handler = (e) => setSystemTheme(e.matches ? 'dark' : 'light'); 12 mediaQuery.addEventListener('change', handler); 13 14 return () => mediaQuery.removeEventListener('change', handler); 15 }, []); 16 17 return systemTheme; 18}

Syncing with Figma#

Tokens Studio Plugin#

1// tokens.json (Figma Tokens Studio format) 2{ 3 "global": { 4 "colors": { 5 "blue": { 6 "500": { 7 "value": "#3b82f6", 8 "type": "color" 9 } 10 } 11 } 12 }, 13 "light": { 14 "brand": { 15 "primary": { 16 "value": "{global.colors.blue.500}", 17 "type": "color" 18 } 19 } 20 } 21}

Automated Sync Workflow#

1# .github/workflows/sync-tokens.yml 2name: Sync Design Tokens 3 4on: 5 push: 6 paths: 7 - 'tokens/**' 8 9jobs: 10 build: 11 runs-on: ubuntu-latest 12 steps: 13 - uses: actions/checkout@v4 14 15 - name: Setup Node 16 uses: actions/setup-node@v4 17 with: 18 node-version: '20' 19 20 - name: Install dependencies 21 run: npm ci 22 23 - name: Build tokens 24 run: npm run build:tokens 25 26 - name: Commit changes 27 run: | 28 git config --local user.email "action@github.com" 29 git config --local user.name "GitHub Action" 30 git add dist/ 31 git diff --staged --quiet || git commit -m "chore: update compiled tokens" 32 git push

Best Practices#

1. Naming Conventions#

1{ 2 // Good: Semantic naming 3 "color-text-primary": "#111827", 4 "color-background-elevated": "#ffffff", 5 6 // Avoid: Value-based naming 7 "color-dark-gray": "#111827", 8 "color-white": "#ffffff" 9}

2. Documentation#

1{ 2 "color": { 3 "brand": { 4 "primary": { 5 "value": "#2563eb", 6 "description": "Primary brand color for buttons and links", 7 "type": "color" 8 } 9 } 10 } 11}

3. Versioning#

1// package.json 2{ 3 "name": "@company/design-tokens", 4 "version": "2.1.0", 5 "main": "dist/js/tokens.js", 6 "files": ["dist"] 7}

Testing Tokens#

1// tokens.test.js 2import tokens from './dist/js/tokens'; 3 4describe('Design Tokens', () => { 5 test('all colors are valid hex values', () => { 6 const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/; 7 8 Object.values(tokens.color).forEach(color => { 9 expect(color).toMatch(hexRegex); 10 }); 11 }); 12 13 test('spacing values are consistent', () => { 14 const spacingValues = Object.values(tokens.spacing); 15 spacingValues.forEach((value, index) => { 16 if (index > 0) { 17 expect(parseInt(value)).toBeGreaterThan(parseInt(spacingValues[index - 1])); 18 } 19 }); 20 }); 21});

Conclusion#

Design tokens bridge the gap between design and development. Start with primitives, build semantic layers, and use tools like Style Dictionary to transform tokens for each platform. Consistent tokens enable scalable design systems that maintain visual coherence across products.

Share this article

Help spread the word about Bootspring