Building Design Systems and Component Libraries at Scale
A comprehensive guide to building design systems with design tokens, component API design, Storybook, versioning, accessibility, and monorepo architecture.
Building Design Systems and Component Libraries at Scale
Every growing product team eventually faces the same problem: inconsistency. Buttons look slightly different across pages. Spacing is inconsistent between sections. The marketing site uses one shade of blue while the application uses another. Designers create components that already exist in code, and developers build UI that diverges from the designs. A design system solves this by establishing a shared language of reusable components, patterns, and guidelines that bridge design and engineering.
But building a design system that teams actually adopt is significantly harder than building a component library. It requires deliberate API design, comprehensive documentation, a sustainable publishing workflow, and - most importantly - a strategy for driving adoption across teams that are already shipping features at full speed. This guide covers the technical architecture and organizational practices that make design systems succeed at scale.
Design System Architecture
A design system is more than a folder of React components. It is a layered architecture with distinct concerns.
Layer 1: Design Tokens - The atomic values that define your visual language: colors, typography, spacing, border radii, shadows, breakpoints, and animation durations. Tokens are platform-agnostic and can be consumed by web, iOS, Android, and email templates.
Layer 2: Core Components - The building blocks: Button, Input, Select, Modal, Tooltip, Card, Badge, Avatar. These components implement the design tokens and provide a consistent API for common UI patterns.
Layer 3: Composite Components - Higher-level components composed from core components: DataTable, CommandPalette, DatePicker, FileUpload, FormField. These encode specific interaction patterns and business logic.
Layer 4: Patterns and Templates - Full-page layouts, navigation patterns, form layouts, and dashboard templates. These are documented as guidelines rather than rigid components, giving product teams flexibility while maintaining consistency.
Design Tokens: The Foundation
Design tokens are named entities that store visual design attributes. They replace hardcoded values with semantic names, making it possible to update the entire visual appearance of your product by changing token values in one place.
Token structure:
Organize tokens in three tiers:
- Global tokens define the raw palette:
color-blue-500: #3B82F6,spacing-4: 16px,font-size-lg: 18px. - Alias tokens map global tokens to semantic purposes:
color-primary: {color-blue-500},spacing-component-gap: {spacing-4}. - Component tokens scope values to specific components:
button-padding-x: {spacing-4},button-bg-primary: {color-primary}.
This layering enables theming. To create a dark mode, you only need to reassign alias tokens - component tokens and code remain unchanged.
Token tooling:
// tokens/color.json (Style Dictionary format)
{
"color": {
"primary": {
"value": "{color.blue.500}",
"type": "color",
"description": "Primary brand color for interactive elements"
},
"primary-hover": {
"value": "{color.blue.600}",
"type": "color"
},
"background": {
"value": "{color.zinc.950}",
"type": "color"
},
"surface": {
"value": "{color.zinc.900}",
"type": "color"
},
"text-primary": {
"value": "{color.zinc.50}",
"type": "color"
}
}
}Use Style Dictionary or Tokens Studio to transform token definitions into platform-specific outputs: CSS custom properties for web, Swift constants for iOS, and XML resources for Android. This ensures a single source of truth for design values across all platforms.
/* Generated CSS custom properties */
:root {
--color-primary: #3B82F6;
--color-primary-hover: #2563EB;
--color-background: #09090B;
--color-surface: #18181B;
--color-text-primary: #FAFAFA;
--spacing-1: 4px;
--spacing-2: 8px;
--spacing-4: 16px;
--spacing-6: 24px;
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
}Component API Design
The API of your components - the props they accept and the patterns they follow - determines whether developers find the design system intuitive or frustrating. A well-designed API is consistent, composable, and hard to misuse.
Principles for component API design:
Consistency across components. If your Button uses variant for visual style, your Badge should too. If Input uses size with values sm, md, lg, every sized component should follow the same convention. Create a prop vocabulary and stick to it.
// Consistent prop conventions across components
<Button variant="primary" size="md" disabled />
<Badge variant="success" size="sm" />
<Input size="md" error="Required field" />
<Select size="md" placeholder="Choose option" />Composition over configuration. Prefer component composition (passing children and sub-components) over deeply nested configuration objects. Composition is more flexible and easier to understand.
// Prefer this (composition)
<Card>
<Card.Header>
<Card.Title>Dashboard</Card.Title>
<Card.Description>Overview of key metrics</Card.Description>
</Card.Header>
<Card.Content>
{/* content */}
</Card.Content>
<Card.Footer>
<Button variant="outline">Cancel</Button>
<Button>Save</Button>
</Card.Footer>
</Card>
// Over this (configuration object)
<Card
title="Dashboard"
description="Overview of key metrics"
content={<div>...</div>}
footer={[
{ label: "Cancel", variant: "outline" },
{ label: "Save" },
]}
/>Polymorphic components with asChild. Allow components to render as different HTML elements or other components without duplicating styles. The asChild pattern (popularized by Radix UI) delegates rendering to the child component.
// Render Button styles on a Link component
<Button asChild>
<Link href="/dashboard">Go to Dashboard</Link>
</Button>Forward refs and spread props. Always forward refs and spread additional HTML attributes onto the root element. This ensures consumers can attach event handlers, ARIA attributes, and data attributes without fighting the component API.
Storybook for Documentation and Development
Storybook serves as the interactive documentation, development environment, and visual testing platform for your design system.
Organizing stories effectively:
- Group stories by the component architecture layers: Tokens, Core, Composite, Patterns.
- Write a default story showing the component in its most common configuration.
- Write variant stories demonstrating each visual variant, size, and state (hover, focus, disabled, loading, error).
- Include an interactive playground with controls (Storybook Controls addon) so consumers can experiment with props.
- Add a documentation page (MDX) for each component explaining when to use it, when not to use it, and accessibility considerations.
Storybook addons to install:
@storybook/addon-a11y- Runs axe accessibility checks on every story automatically.@storybook/addon-interactions- Tests user interactions directly in Storybook.@storybook/addon-viewport- Preview components at different screen sizes.chromatic- Visual regression testing that catches unintended visual changes on every PR.
Chromatic for visual regression testing: Connect Storybook to Chromatic to automatically capture screenshots of every story on every PR. Chromatic diffs the screenshots against the baseline and flags visual changes for review. This catches unintended side effects - a spacing change in a shared token that subtly breaks the layout of a distant component.
Versioning and Publishing
Your design system is a dependency for other teams. Treating it as a properly versioned, published package is essential for stability and trust.
Semantic versioning (semver) guidelines for design systems:
- Patch (1.0.x): Bug fixes, documentation updates, and internal refactors with no API changes.
- Minor (1.x.0): New components, new props on existing components, new token values. Backward-compatible.
- Major (x.0.0): Removed components or props, renamed tokens, breaking API changes. These should be rare and well-communicated with migration guides.
Publishing workflow:
# Typical release workflow using changesets
# 1. Developer creates a changeset describing their change
npx changeset
# Prompts: which packages changed, semver bump type, description
# 2. CI collects changesets and creates a release PR
# 3. Merging the release PR publishes to npm and creates GitHub releases
# 4. Consuming applications update the dependency at their own paceUse Changesets (by the Atlassian team) to manage versioning in a monorepo. Each PR that modifies the design system includes a changeset file describing the change and the appropriate semver bump. The release process aggregates changesets into a CHANGELOG, bumps versions, publishes to npm, and creates GitHub releases - all automated through CI.
Provide codemods for breaking changes. When a major version renames a prop or removes a component, ship a codemod (using jscodeshift or ts-morph) that automatically updates consuming codebases. This dramatically reduces the friction of upgrading.
Accessibility in Design Systems
A design system is the highest-leverage place to invest in accessibility because every component is used across the entire product surface. Getting accessibility right in the design system means every consuming team inherits accessible patterns by default.
Accessibility checklist for components:
- Keyboard navigation. Every interactive component must be operable with keyboard alone. Implement proper focus management for modals, dropdowns, and complex widgets.
- ARIA attributes. Use appropriate ARIA roles, states, and properties. Leverage libraries like Radix UI or React Aria that handle ARIA correctly out of the box.
- Color contrast. Ensure all text meets WCAG 2.1 AA contrast ratios (4.5:1 for normal text, 3:1 for large text). Validate token combinations, not just individual colors.
- Focus indicators. Provide visible focus indicators that meet the 3:1 contrast ratio against adjacent colors. Never remove focus outlines without providing an alternative.
- Screen reader testing. Test components with VoiceOver (macOS), NVDA (Windows), and TalkBack (Android). Automated tools catch about 30 percent of accessibility issues - manual testing catches the rest.
- Motion preferences. Respect
prefers-reduced-motionfor all animations. Provide a mechanism to disable motion globally.
// Respecting motion preferences
const prefersReducedMotion =
typeof window !== 'undefined' &&
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
const animationConfig = prefersReducedMotion
? { duration: 0 }
: { duration: 0.2, ease: 'easeOut' };Monorepo Setup for Shared Components
A monorepo is the natural home for a design system because it co-locates the tokens, components, documentation, and tooling in a single repository while allowing independent versioning and publishing.
Recommended monorepo structure:
packages/
tokens/ # Design token definitions and build scripts
src/
package.json # @company/design-tokens
ui/ # Core and composite components
src/
components/
button/
button.tsx
button.test.tsx
button.stories.tsx
index.ts
index.ts # Public API barrel export
package.json # @company/ui
icons/ # Icon library
package.json # @company/icons
eslint-config/ # Shared ESLint configuration
tsconfig/ # Shared TypeScript configuration
apps/
docs/ # Storybook documentation site
playground/ # Development sandbox
Tooling for monorepo management:
- Turborepo or Nx for task orchestration (build, test, lint across packages with caching).
- pnpm workspaces for dependency management (faster installs, strict dependency resolution).
- TypeScript project references for incremental type checking across packages.
- tsup or unbuild for building packages (bundles ESM and CJS, generates type declarations).
Build pipeline:
# Build tokens first (other packages depend on generated CSS/JS)
# Then build UI components
# Then build documentation site
turbo run build --filter=@company/design-tokens
turbo run build --filter=@company/ui
turbo run build --filter=docs