ncremental builds that scale with team size.
// turbo.json
{
"$schema": "https://turbo.build/schema.json",
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"]
},
"test": {
"dependsOn": ["build"],
"outputs": []
},
"lint": {
"outputs": []
},
"storybook:build": {
"dependsOn": ["build"],
"outputs": ["storybook-static/**"]
}
}
}
Rationale: Monorepos enforce shared tooling, prevent version mismatches, and allow atomic commits that update tokens, components, and documentation simultaneously. Turborepo's caching reduces CI time by 60β80% in medium-to-large systems.
2. Design Token Pipeline
Tokens must be framework-agnostic and generated at build time. Style Dictionary or Token Studio pipelines convert design values into CSS variables, TypeScript types, and platform-specific formats.
// packages/tokens/src/build.ts
import StyleDictionary from 'style-dictionary';
import { resolve } from 'path';
const sd = new StyleDictionary({
source: [resolve(__dirname, 'tokens/**/*.json')],
platforms: {
css: {
transformGroup: 'css',
buildPath: 'dist/css/',
files: [{ destination: 'variables.css', format: 'css/variables' }]
},
ts: {
transformGroup: 'js',
buildPath: 'dist/ts/',
files: [{ destination: 'tokens.d.ts', format: 'typescript/export-declarations' }]
}
}
});
sd.buildAllPlatforms();
Rationale: Compile-time token generation eliminates runtime overhead, ensures type safety, and decouples design values from implementation. CSS variables remain for dynamic theming, but base values are immutable contracts.
3. Component Architecture: Headless + Compound Pattern
Components must separate logic from presentation. Headless hooks manage state and accessibility; UI wrappers apply tokens. Compound components enforce valid composition patterns.
// packages/ui/src/button/index.ts
export { Button } from './Button';
export { ButtonGroup } from './ButtonGroup';
export { useButtonGroup } from './useButtonGroup';
// packages/ui/src/button/Button.tsx
import { forwardRef, ButtonHTMLAttributes } from 'react';
import { cx } from 'class-variance-authority';
import { token } from '@design-system/tokens';
export interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = 'primary', size = 'md', className, ...props }, ref) => {
return (
<button
ref={ref}
className={cx(
token('components.button.base'),
token(`components.button.variants.${variant}`),
token(`components.button.sizes.${size}`),
className
)}
{...props}
/>
);
}
);
Button.displayName = 'Button';
Rationale: class-variance-authority (CVA) provides type-safe variant composition without CSS-in-JS runtime costs. Forward refs preserve DOM interop. Headless logic is extracted to hooks when state complexity exceeds presentation needs.
4. Theming & CSS Strategy
Use CSS custom properties for runtime theming, but generate them from tokens. Avoid inline styles or runtime CSS-in-JS for core components.
/* dist/css/variables.css (generated) */
:root {
--ds-color-primary: #0f172a;
--ds-color-primary-hover: #1e293b;
--ds-radius-md: 0.375rem;
--ds-font-weight-medium: 500;
}
// packages/ui/src/theme/ThemeProvider.tsx
import { createContext, useContext, useEffect, useState } from 'react';
import type { ReactNode } from 'react';
interface ThemeContextValue {
theme: 'light' | 'dark';
setTheme: (t: 'light' | 'dark') => void;
}
const ThemeContext = createContext<ThemeContextValue>({
theme: 'light',
setTheme: () => {}
});
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<'light' | 'dark'>('light');
useEffect(() => {
document.documentElement.setAttribute('data-theme', theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{children}
</ThemeContext.Provider>
);
}
export const useTheme = () => useContext(ThemeContext);
Rationale: CSS variables provide zero-runtime theming, support system preferences, and enable server-side rendering without hydration mismatches. Context manages application-level overrides without leaking into component internals.
5. Testing & Automation
Visual regression, unit tests, and automated publishing form the safety net. Vitest handles logic; Playwright handles DOM; Chromatic or Playwright visual handles rendering.
// packages/ui/src/button/Button.test.tsx
import { render, screen } from '@testing-library/react';
import { Button } from './Button';
describe('Button', () => {
it('renders with default variant', () => {
render(<Button>Click</Button>);
expect(screen.getByRole('button')).toHaveTextContent('Click');
expect(screen.getByRole('button')).toHaveAttribute('type', 'button');
});
it('applies correct variant classes', () => {
const { container } = render(<Button variant="secondary">Click</Button>);
const btn = container.firstChild as HTMLElement;
expect(btn.className).toContain('ds-button--secondary');
});
});
Rationale: Unit tests validate behavior; visual tests catch unintended style regressions; automated semantic release enforces versioning discipline. All three are non-negotiable for production systems.
Pitfall Guide
-
Treating the system as a UI kit instead of a contract
Components without TypeScript interfaces, versioning, or breaking-change policies become implementation details rather than platform standards. Downstream teams will override styles, duplicate logic, and ignore updates. Fix: enforce strict typing, semantic versioning, and changelog generation.
-
Over-engineering flexibility with prop drilling
Adding className, style, and dozens of variant props defeats the purpose of a design system. It encourages style leakage and makes visual testing impossible. Fix: use CVA or similar variant engines, restrict className to layout slots, and expose explicit composition APIs.
-
Ignoring accessibility until late in the cycle
A11y cannot be retrofitted. Missing aria-* attributes, focus management, and keyboard navigation create compliance risks and degrade UX. Fix: integrate @testing-library/jest-dom a11y rules, enforce focus traps in modals, and use headless libraries like Radix or Ariakit for complex patterns.
-
Skipping automated visual regression testing
Manual QA misses pixel-level drift. CSS variable changes, font updates, and framework upgrades silently break rendering. Fix: run Playwright visual tests or Chromatic on every PR. Fail CI on unapproved diffs.
-
No versioning or breaking-change strategy
Patching major changes or skipping semver causes downstream breakage. Teams will fork the system or pin to outdated versions. Fix: use semantic-release or changesets. Document deprecations with 6-month migration windows.
-
Coupling tokens to a single framework
Hardcoding tokens in React Context or Vue composables prevents cross-framework adoption. Design systems must outlive framework choices. Fix: generate framework-agnostic JSON/CSS tokens. Provide lightweight adapters for each target stack.
-
Neglecting adoption metrics and feedback loops
Publishing components without tracking usage leads to unused APIs and abandoned patterns. Fix: instrument Storybook analytics, track npm download velocity, and maintain a public roadmap with quarterly deprecation cycles.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Single product, small team | Component library + Storybook | Low overhead, fast iteration | Minimal |
| Multi-product enterprise | Monorepo + token pipeline + changesets | Enforces contracts, scales governance | Moderate upfront, low long-term |
| Cross-framework adoption (React/Vue/Angular) | Framework-agnostic tokens + headless logic | Prevents vendor lock-in, maximizes reuse | High initial engineering |
| Regulated industry (healthcare/finance) | Strict a11y gates + visual regression + semver | Compliance, auditability, risk reduction | High testing overhead |
| Rapid prototyping / startup | Design tokens + UI kit + manual publishing | Speed over governance | Low initial, high drift risk |
Configuration Template
// package.json (root)
{
"name": "design-system",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "turbo run build",
"test": "turbo run test",
"lint": "turbo run lint",
"storybook": "turbo run storybook",
"release": "changeset publish",
"version": "changeset version"
},
"devDependencies": {
"turbo": "^2.0.0",
"@changesets/cli": "^2.27.0",
"style-dictionary": "^3.9.0"
}
}
// packages/tokens/package.json
{
"name": "@design-system/tokens",
"version": "1.0.0",
"main": "dist/ts/tokens.js",
"types": "dist/ts/tokens.d.ts",
"scripts": {
"build": "ts-node src/build.ts",
"prepublishOnly": "npm run build"
},
"files": ["dist"]
}
Quick Start Guide
- Run
npx create-turbo@latest design-system and select TypeScript workspace.
- Add
style-dictionary and class-variance-authority to the root devDependencies.
- Create
packages/tokens with tokens/ directory, run npm run build, and verify CSS/TS output.
- Scaffold
packages/ui with Vite + React, implement Button using CVA, and write Vitest tests.
- Run
npx changeset init, create a .changeset/ file, and execute npm run version && npm run release to publish your first versioned package.