Skip to main content

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.

Further Reading