Design Tokens and CSS Systems for Monorepo Branding
Design tokens are a single source of truth for design decisions: colors, typography, spacing, shadows, and animations. Instead of hard-coding #007bff in ten places, you define a blue token once and use it everywhere. When your brand updates blue, you change one token and all apps update automatically. This article shows you how to build a design token package, export tokens as CSS variables and TypeScript constants, and integrate them into a shared component library.
Understanding Design Tokens
A design token represents a design decision as a key-value pair. Common token categories include colors (primary, secondary, danger), typography (font family, sizes, weights), spacing (gap, padding, margin scales), shadows, border radius, and animations. Tokens live in a dedicated package in your monorepo, separate from components.
Creating a Design Tokens Package
Start by creating the tokens package:
mkdir -p packages/design-tokens
cd packages/design-tokens
pnpm init
Edit packages/design-tokens/package.json:
{
"name": "@myapp/design-tokens",
"version": "1.0.0",
"description": "Centralized design tokens for color, typography, spacing",
"type": "module",
"exports": {
".": "./dist/index.js",
"./css": "./dist/tokens.css",
"./json": "./dist/tokens.json"
},
"scripts": {
"build": "node build.js",
"dev": "node build.js --watch"
},
"devDependencies": {
"typescript": "workspace:*"
}
}
Create a src/tokens.ts file that defines all tokens as JavaScript objects:
// packages/design-tokens/src/tokens.ts
export const colors = {
primary: '#007bff',
secondary: '#6c757d',
success: '#28a745',
danger: '#dc3545',
warning: '#ffc107',
info: '#17a2b8',
light: '#f8f9fa',
dark: '#343a40',
};
export const typography = {
fontFamily: {
base: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
mono: '"SF Mono", Monaco, "Cascadia Code", Roboto Mono, monospace',
},
fontSize: {
xs: '0.75rem',
sm: '0.875rem',
base: '1rem',
lg: '1.125rem',
xl: '1.25rem',
'2xl': '1.5rem',
'3xl': '1.875rem',
},
fontWeight: {
light: 300,
normal: 400,
semibold: 600,
bold: 700,
},
};
export const spacing = {
0: '0',
1: '0.25rem',
2: '0.5rem',
3: '0.75rem',
4: '1rem',
6: '1.5rem',
8: '2rem',
12: '3rem',
16: '4rem',
};
export const borderRadius = {
sm: '0.125rem',
base: '0.25rem',
md: '0.5rem',
lg: '0.75rem',
full: '9999px',
};
export const shadows = {
none: 'none',
sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
base: '0 1px 3px 0 rgba(0, 0, 0, 0.1)',
md: '0 4px 6px -1px rgba(0, 0, 0, 0.1)',
lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1)',
};
Building Tokens into Multiple Formats
Create a build script that generates CSS variables, JSON, and TypeScript exports. Create packages/design-tokens/build.js:
// packages/design-tokens/build.js
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const tokens = await import('./src/tokens.ts');
// Generate CSS variables
function generateCSS() {
let css = ':root {\n';
Object.entries(tokens.colors).forEach(([key, value]) => {
css += ` --color-${key}: ${value};\n`;
});
Object.entries(tokens.typography.fontSize).forEach(([key, value]) => {
css += ` --font-size-${key}: ${value};\n`;
});
Object.entries(tokens.spacing).forEach(([key, value]) => {
css += ` --spacing-${key}: ${value};\n`;
});
Object.entries(tokens.shadows).forEach(([key, value]) => {
css += ` --shadow-${key}: ${value};\n`;
});
css += '}\n';
return css;
}
// Generate JSON
function generateJSON() {
return JSON.stringify(tokens, null, 2);
}
// Write outputs
const distDir = path.join(__dirname, 'dist');
if (!fs.existsSync(distDir)) fs.mkdirSync(distDir, { recursive: true });
fs.writeFileSync(path.join(distDir, 'tokens.css'), generateCSS());
fs.writeFileSync(path.join(distDir, 'tokens.json'), generateJSON());
console.log('Tokens built successfully!');
Run the build:
pnpm build
This creates dist/tokens.css with CSS variables and dist/tokens.json with token objects.
Exporting TypeScript Tokens
Add TypeScript exports to packages/design-tokens/src/index.ts:
// packages/design-tokens/src/index.ts
export * from './tokens';
// Re-export as constants for type-safe usage
export const tokens = {
colors: import('./tokens').then(m => m.colors),
typography: import('./tokens').then(m => m.typography),
spacing: import('./tokens').then(m => m.spacing),
};
Integrating Tokens into Components
Update your component library to depend on design tokens. Edit packages/ui-library/package.json:
{
"dependencies": {
"@myapp/design-tokens": "workspace:*"
}
}
Use tokens in your Button component:
// packages/ui-library/src/components/Button.tsx
import { colors, spacing } from '@myapp/design-tokens';
import React from 'react';
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'danger';
}
export const Button: React.FC<ButtonProps> = ({
variant = 'primary',
children,
style = {},
...rest
}) => {
const bgColor = {
primary: colors.primary,
secondary: colors.secondary,
danger: colors.danger,
}[variant];
return (
<button
style={{
backgroundColor: bgColor,
padding: `${spacing[2]} ${spacing[4]}`,
...style,
}}
{...rest}
>
{children}
</button>
);
};
Applying Tokens Globally with CSS Variables
In your consuming app, import the tokens CSS to apply variables globally:
// apps/web/src/main.tsx
import '@myapp/design-tokens/css';
import React from 'react';
import ReactDOM from 'react-dom/client';
import { App } from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>
);
Now use CSS variables in any stylesheet:
/* apps/web/src/styles/app.css */
body {
font-family: var(--font-family-base);
color: var(--color-dark);
background-color: var(--color-light);
}
.card {
padding: var(--spacing-4);
border-radius: var(--border-radius-lg);
box-shadow: var(--shadow-md);
}
Updating Tokens Across All Apps
When you update a token, rebuild and all apps using that token reflect the change. Update packages/design-tokens/src/tokens.ts:
export const colors = {
primary: '#0050f0', // Updated from #007bff
// ... rest
};
Run pnpm build and bump the version. All apps automatically use the new blue.
Key Takeaways
- Centralize all design decisions (colors, spacing, typography) in a dedicated tokens package.
- Build tokens into multiple formats: CSS variables for stylesheets, JSON for tooling, and TypeScript for type-safe component code.
- Use the
workspace:*protocol to link the tokens package in all dependent packages and apps. - Import CSS variables globally in your app root and use them throughout component styles.
- Update tokens once and instantly apply changes across your entire organization.
Frequently Asked Questions
How do I organize tokens for dark mode?
Create separate token files: tokens-light.ts and tokens-dark.ts. Build both to CSS, then use CSS media queries or a class-based toggle to switch between them. Use CSS custom properties with fallbacks.
Can I use tokens with Tailwind CSS?
Yes. Add tokens to your Tailwind config under theme.extend. Reference tokens in class names or use @apply directives in component CSS.
How do I version tokens independently from components?
Keep them in separate packages with separate version numbers. Update packages/design-tokens/package.json independently from packages/ui-library/package.json. This lets you release token updates without changing component code.
What if different apps need different token values?
Create variant token files: tokens-base.ts, tokens-app-a.ts, tokens-app-b.ts. Build each variant to CSS and have each app import its own. Or use CSS custom property overrides at the app level.
How do I ensure token names are semantic?
Use names like color-primary-hover instead of color-blue-600. Semantic names describe the intent (primary button color), not the value (blue). This makes rebranding easier.