rem', 4: '1rem', 8: '2rem' },
radius: { sm: '0.125rem', md: '0.375rem', lg: '0.5rem' }
} as const;
// packages/design-tokens/src/semantic.ts
import { primitives } from './primitives';
export const semantic = {
color: {
background: {
primary: primitives.color.gray[50],
surface: '#ffffff'
},
text: {
primary: primitives.color.gray[900],
secondary: primitives.color.gray[500]
},
border: {
default: primitives.color.gray[500],
focus: primitives.color.blue[500]
}
},
radius: {
interactive: primitives.radius.sm,
container: primitives.radius.md
}
} as const;
// Generate CSS Variables for runtime usage
export const cssVariables = :root { --color-background-primary: ${semantic.color.background.primary}; --color-text-primary: ${semantic.color.text.primary}; --radius-interactive: ${semantic.radius.interactive}; };
**Rationale:** Using `as const` ensures TypeScript infers literal types, enabling autocomplete and compile-time checks. Separating primitives from semantics allows for dark mode and high-contrast themes by remapping semantic values without touching component code.
#### 2. Component Primitives with Headless Patterns
Components should separate behavior from styling. Headless primitives manage state, accessibility (ARIA), and keyboard navigation, while styling is applied via composition. This pattern supports multiple styling strategies (CSS Modules, Tailwind, CSS-in-JS) and ensures accessibility compliance is centralized.
**Implementation:**
```typescript
// packages/design-system/src/components/button/button.tsx
import * as React from 'react';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/utils/cn';
// Variants defined via CVA for type-safe class composition
const buttonVariants = cva(
'inline-flex items-center justify-center font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
primary: 'bg-blue-500 text-white hover:bg-blue-600 focus-visible:ring-blue-500',
secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-400',
ghost: 'hover:bg-gray-100 text-gray-700 focus-visible:ring-gray-400'
},
size: {
sm: 'h-8 px-3 text-sm rounded-sm',
md: 'h-10 px-4 py-2 text-sm rounded-md',
lg: 'h-12 px-6 text-base rounded-lg'
}
},
defaultVariants: {
variant: 'primary',
size: 'md'
}
}
);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, ...props }, ref) => {
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
);
}
);
Button.displayName = 'Button';
Rationale:
- Class Variance Authority (CVA): Provides a type-safe way to define component variants. The
VariantProps type ensures that only valid combinations of variant and size are accepted by the compiler.
- Forward Ref: Enables parent components to manage focus and interact with the DOM node, essential for focus management in modals and dialogs.
- Accessibility: The base classes include
focus-visible states and disabled handling. For complex components, integrate @radix-ui primitives to handle focus trapping, aria attributes, and keyboard events.
3. Monorepo Architecture
A design system must be developed alongside consuming applications. A monorepo structure using Turborepo or Nx allows shared tooling, atomic changes, and immediate feedback loops.
Structure:
design-system/
βββ packages/
β βββ design-tokens/ # Tokens and CSS variables
β βββ design-system/ # Components and hooks
β βββ eslint-config/ # Shared linting rules
β βββ typescript-config/ # Shared TS configs
βββ apps/
β βββ docs/ # Storybook / Documentation site
β βββ web/ # Consumer application
βββ turbo.json
βββ package.json
Rationale: This structure enables turborepo to cache builds and run tasks in parallel. Changes to tokens trigger rebuilds only in dependent packages. The docs app serves as the living documentation and playground, ensuring components are tested in isolation.
4. Testing and Quality Assurance
A design system without rigorous testing is a liability. Implement three layers of testing:
- Unit Tests: Validate logic, props, and event handlers using Vitest or Jest.
- Accessibility Tests: Automated axe-core integration in unit tests.
- Visual Regression: Chromatic or Percy to detect unintended UI changes.
// packages/design-system/src/components/button/button.test.tsx
import { render, screen } from '@testing-library/react';
import { axe, toHaveNoViolations } from 'jest-axe';
import { Button } from './button';
expect.extend(toHaveNoViolations);
describe('Button', () => {
it('renders correctly', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument();
});
it('has no accessibility violations', async () => {
const { container } = render(<Button>Accessible Button</Button>);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('handles disabled state', () => {
render(<Button disabled>Disabled</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Pitfall Guide
Production experience reveals recurring anti-patterns that degrade design system quality and adoption.
-
The "Kitchen Sink" Anti-Pattern
- Mistake: Attempting to build every possible component before launching.
- Impact: Delays adoption, increases maintenance burden for unused code, and misaligns with actual product needs.
- Best Practice: Build only what is needed. Extract components from real usage. A minimal viable system with 10 well-engineered components outperforms a library of 100 incomplete ones.
-
Ignoring the Semantic Token Layer
- Mistake: Mapping primitives directly to components (e.g.,
bg-blue-500).
- Impact: Impossible to support themes, dark mode, or context-specific overrides without refactoring components.
- Best Practice: Always use semantic tokens in components.
bg-background-primary allows the design system to change the definition of "primary background" globally without touching component code.
-
Over-Engineering Variants
- Mistake: Creating a variant for every edge case (e.g.,
variant="primary-with-icon-and-loading").
- Impact: Combinatorial explosion of props, reduced type safety, and harder maintenance.
- Best Practice: Favor composition over variants. Provide a
Button and an Icon component, and let consumers compose them. Use slots or children for flexible content.
-
Neglecting Accessibility (a11y)
- Mistake: Treating accessibility as a visual checklist or post-launch audit.
- Impact: Legal risk, exclusion of users, and costly retrofits.
- Best Practice: Integrate a11y into the development workflow. Use headless primitives that enforce ARIA attributes. Run automated a11y tests in CI. Manually test with screen readers for complex interactions.
-
Lack of Governance and Contribution Model
- Mistake: A central team builds the system, but no process exists for others to contribute or request changes.
- Impact: Bottlenecks, frustration, and shadow libraries. Teams fork components to meet deadlines.
- Best Practice: Establish a clear contribution guide. Implement a review process for changes. Create a feedback loop where consuming teams can propose enhancements. Treat the design system team as enablers, not gatekeepers.
-
Token Sprawl
- Mistake: Creating unique tokens for every new UI requirement without consolidation.
- Impact: Inconsistency, bloated CSS, and loss of the "single source of truth" benefit.
- Best Practice: Enforce token reuse. When a new design requirement arises, map it to existing tokens or extend the semantic layer deliberately. Regularly audit tokens for duplicates or unused values.
-
Skipping Versioning Strategy
- Mistake: Pushing breaking changes to the main branch without versioning.
- Impact: Consumer applications break unexpectedly. Teams hesitate to update.
- Best Practice: Use Semantic Versioning (SemVer). Major versions for breaking changes, minor for features, patch for fixes. Automate changelog generation. Provide migration guides for major updates.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Startup / Single Product | CSS Modules + CVA + Figma Library | Low overhead, rapid iteration, sufficient for small teams. | Low initial, Low maintenance. |
| Enterprise / Multi-Platform | Web Components + Design Tokens + Headless Primitives | Framework agnostic, enables reuse across web, mobile, and native apps. | High initial, Low long-term scaling cost. |
| Marketing / Content Sites | UI Kit + Static Site Generator | Focus on content velocity; design system complexity is unnecessary overhead. | Low. |
| Regulated / High-A11y Requirements | Radix UI + Strict TS Config + Automated A11y CI | Guarantees accessibility compliance and type safety for critical interactions. | Medium initial, High risk mitigation. |
Configuration Template
turbo.json
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
},
"test": {
"dependsOn": ["build"],
"inputs": ["src/**/*.tsx", "src/**/*.ts", "test/**/*.ts"]
},
"dev": {
"cache": false,
"persistent": true
}
}
}
packages/design-system/tsconfig.json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
Quick Start Guide
-
Initialize Repository:
npx create-turbo@latest design-system --package-manager pnpm
cd design-system
-
Add Dependencies:
pnpm add -w class-variance-authority clsx tailwind-merge @radix-ui/react-slot
pnpm add -D tailwindcss postcss autoprefixer
-
Configure Tailwind:
Create tailwind.config.ts in the design-system package to extend classes with design tokens.
import type { Config } from 'tailwindcss';
import { semantic } from '@repo/design-tokens';
export default {
content: ['./src/**/*.{ts,tsx}'],
theme: {
extend: {
colors: semantic.color,
borderRadius: semantic.radius
}
}
} satisfies Config;
-
Create First Component:
Generate a Button component using the pattern defined in Core Solution. Verify it renders in the docs app using Storybook.
-
Consume in App:
In apps/web, import the button:
import { Button } from '@repo/design-system';
export default function Home() {
return <Button variant="primary">Get Started</Button>;
}
Run pnpm dev to see the component with live token updates.