Design System Engineering: Token-Based Theming, Component Library, Storybook, and Chromatic
Build a production design system: implement design token architecture with CSS custom properties, create a typed React component library, document with Storybook, automate visual regression testing with Chromatic, and publish with semantic versioning.
Design System Engineering: Building Component Libraries at Scale (2026)
A design system is more than a collection of components. It's the bridge between design intent and implementation reality. After building and maintaining design systems for clients ranging from startups to enterprises, I've learned that the difference between a successful system and one that gets ignored is architectural discipline.
The Problem with Ad-Hoc Component Growth
Most teams don't plan to have a mess. It happens naturally. Early in a project, you build a few buttons and cards. Weeks later, designers and developers have made slightly different versions of the same component. By month three, you have five button variants nobody can explain. By year two, new team members don't even know what's available in the "system," so they just build new components.
This fragmentation has real costs:
- Design inconsistency damages user trust
- Developers waste time rebuilding things that exist
- Maintenance becomes a nightmare (fix a bug in one button, miss it in three others)
- New feature development slows down as teams navigate component confusion
- Testing and QA multiply unnecessarily
We've seen projects where the design system became so tangled that the team eventually gave up and migrated to a new framework entirelyβat enormous cost. It doesn't have to be this way.
Architectural Foundations for Design Systems
Before writing a single component, you need a solid architecture. This is where most teams stumble.
Monorepo vs. Separate Repository
For web development projects, we've converged on monorepo architecture:
Code:
design-system/
βββ packages/
β βββ core/ # Base tokens, utilities
β βββ components/ # React components
β βββ tokens/ # Design tokens
β βββ docs/ # Storybook
β βββ cli/ # Code generation tools
βββ pnpm-workspace.yaml
βββ turbo.json
Monorepo advantages:
- Atomic changes across packages
- Shared utilities and types
- Single source of truth for dependencies
- Easier discoverability
Single repository approach:
- Simpler for small teams
- Easier to enforce consistency
- Single CI/CD pipeline
- But doesn't scale past a team of 5-10
We typically use Turbo for task orchestration and pnpm for package management.
Token Architecture
Design tokens are the foundation. They represent every decision: color, spacing, typography, shadows.
Code:
// tokens/src/color.ts
export const colors = {
// Semantic tokens
'color-text-primary': '#1a1a1a',
'color-text-secondary': '#666666',
'color-background-primary': '#ffffff',
'color-background-secondary': '#f5f5f5',
// Brand tokens
'color-brand-primary': '#0066cc',
'color-brand-secondary': '#ff6600',
// State tokens
'color-success': '#22c55e',
'color-error': '#ef4444',
'color-warning': '#f59e0b'
};
export const spacing = {
'space-xs': '4px',
'space-sm': '8px',
'space-md': '16px',
'space-lg': '32px',
'space-xl': '64px'
};
export const typography = {
'font-family-primary': "'Inter', sans-serif",
'font-size-sm': '12px',
'font-size-base': '14px',
'font-size-lg': '16px',
'font-weight-regular': 400,
'font-weight-medium': 500,
'font-weight-bold': 700
};
These tokens should be generated into:
- CSS variables
- Tailwind configuration
- Design tool files (Figma tokens)
- JavaScript objects
- Documentation
A single source of truth prevents the inevitable drift between design and code.
π Looking for a Dev Team That Actually Delivers?
Most agencies sell you a project manager and assign juniors. Viprasol is different β senior engineers only, direct Slack access, and a 5.0β Upwork record across 1000+ projects.
- React, Next.js, Node.js, TypeScript β production-grade stack
- Fixed-price contracts β no surprise invoices
- Full source code ownership from day one
- 90-day post-launch support included
Component Structure and Documentation
Each component should have clear responsibilities:
Code:
components/button/
βββ Button.tsx # Component implementation
βββ Button.test.tsx # Unit tests
βββ Button.stories.tsx # Storybook stories
βββ Button.module.css # Styles
βββ types.ts # TypeScript definitions
βββ index.ts # Public API
βββ README.md # Usage documentation
Documentation is critical. For each component:
Code:
# Button Component
## Overview
Primary interactive element for user actions.
## API
- **variant**: 'primary' | 'secondary' | 'tertiary'
- **size**: 'small' | 'medium' | 'large'
- **disabled**: boolean
- **loading**: boolean
## Usage
\**\**\'tsx
<Button variant="primary" onClick={handleClick}>
Click me
</Button>
\**\**\'
## Accessibility
- Supports keyboard navigation
- Screen reader friendly
- High contrast support
- Meets WCAG 2.1 AA standards
## Implementation Notes
- Uses CSS modules for styling
- Accepts standard HTML button attributes
- Loading state managed by parent
Component Composition Patterns
Base Components (Atoms)
Simple, single-responsibility components:
Code:
export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'tertiary';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
export function Button({
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
children
}: ButtonProps) {
return (
<button
className={classNames(
styles.button,
styles[**variant-${variant}**],
styles[**size-${size}**],
disabled && styles.disabled
)}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Composed Components (Molecules)
Combinations of base components:
Code:
export interface CardProps {
title: string;
description?: string;
action?: React.ReactNode;
}
export function Card({ title, description, action }: CardProps) {
return (
<div className={styles.card}>
<div className={styles.header}>
<Heading level="h3">{title}</Heading>
{action && <div className={styles.action}>{action}</div>}
</div>
{description && <Text color="secondary">{description}</Text>}
</div>
);
}
Patterns and Templates (Organisms)
Complex patterns that guide page structures:
Code:
export interface PageLayoutProps {
children: React.ReactNode;
sidebar?: React.ReactNode;
header?: React.ReactNode;
}
export function PageLayout({ children, sidebar, header }: PageLayoutProps) {
return (
<div className={styles.layout}>
{header && <header className={styles.pageHeader}>{header}</header>}
<div className={styles.content}>
{sidebar && <aside className={styles.sidebar}>{sidebar}</aside>}
<main className={styles.main}>{children}</main>
</div>
</div>
);
}

π Senior Engineers. No Junior Handoffs. Ever.
You get the senior developer, not a project manager who relays your requirements to someone you never meet. Every Viprasol project has a senior lead from kickoff to launch.
- MVPs in 4β8 weeks, full platforms in 3β5 months
- Lighthouse 90+ performance scores standard
- Works across US, UK, AU timezones
- Free 30-min architecture review, no commitment
Recommended Reading
Theming and Customization
Support themes without component explosion:
Code:
// contexts/theme.tsx
export interface Theme {
colors: Record<string, string>;
spacing: Record<string, string>;
typography: Record<string, string>;
}
export const ThemeContext = createContext<Theme>(defaultTheme);
export function ThemeProvider({ theme, children }) {
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
}
// components/Button.tsx
export function Button(props: ButtonProps) {
const theme = useContext(ThemeContext);
return (
<button style={{
color: theme.colors['color-text-primary'],
padding: theme.spacing['space-md']
}}>
{props.children}
</button>
);
}
For SaaS development clients, theming lets us support white-label solutions without duplicating components.
Testing and Quality Assurance
Component testing should cover three levels:
Unit Tests
Code:
describe('Button', () => {
it('renders with correct text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('calls onClick handler when clicked', () => {
const handleClick = jest.fn();
render(<Button onClick={handleClick}>Click me</Button>);
fireEvent.click(screen.getByRole('button'));
expect(handleClick).toHaveBeenCalled();
});
it('applies disabled state', () => {
render(<Button disabled>Click me</Button>);
expect(screen.getByRole('button')).toBeDisabled();
});
});
Visual Regression Tests
Code:
describe('Button visual regression', () => {
it('matches snapshot', () => {
const { container } = render(
<Button variant="primary">Primary Button</Button>
);
expect(container).toMatchSnapshot();
});
it('matches with different variants', () => {
const variants = ['primary', 'secondary', 'tertiary'];
variants.forEach(variant => {
const { container } = render(
<Button variant={variant}>{variant}</Button>
);
expect(container).toMatchImageSnapshot();
});
});
});
Accessibility Tests
Code:
describe('Button accessibility', () => {
it('is keyboard accessible', () => {
render(<Button>Click me</Button>);
const button = screen.getByRole('button');
button.focus();
expect(button).toHaveFocus();
});
it('has accessible name', () => {
render(<Button>Click me</Button>);
expect(screen.getByRole('button', { name: /click me/i }))
.toBeInTheDocument();
});
it('announces loading state', () => {
render(<Button loading>Processing</Button>);
expect(screen.getByRole('button'))
.toHaveAttribute('aria-busy', 'true');
});
});
Versioning and Deprecation
Design systems evolve. Plan for this:
Code:
// Deprecated component - still works but shows warning
export const OldButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props, ref) => {
console.warn(
'OldButton is deprecated. Use Button instead. ' +
'OldButton will be removed in v2.0.0'
);
return <Button {...props} ref={ref} />;
}
);
Use semantic versioning:
- Major (v1.0.0 β v2.0.0): Breaking changes
- Minor (v1.0.0 β v1.1.0): New components/features
- Patch (v1.0.0 β v1.0.1): Bug fixes
Documentation and Discovery
For cloud solutions teams with multiple services, a living style guide is essential:
Code:
// storybook/Button.stories.tsx
export default {
title: 'Components/Button',
component: Button,
argTypes: {
variant: {
control: { type: 'select' },
options: ['primary', 'secondary', 'tertiary']
},
size: {
control: { type: 'select' },
options: ['small', 'medium', 'large']
},
disabled: { control: 'boolean' }
}
};
export const Primary = {
args: { variant: 'primary', children: 'Primary Button' }
};
export const Secondary = {
args: { variant: 'secondary', children: 'Secondary Button' }
};
export const Disabled = {
args: { variant: 'primary', disabled: true, children: 'Disabled' }
};
export const AllVariants = {
render: () => (
<div style={{ display: 'flex', gap: '16px' }}>
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
</div>
)
};
Governance and Contribution
Without governance, design systems become dumping grounds:
Contribution Process
- Design review: Does this fit the system?
- Code review: Does it meet our quality standards?
- Documentation: Is it documented and discoverable?
- Testing: Is it tested adequately?
- Approval: Does it get the maintainer sign-off?
Maintenance Schedule
Dedicate time to:
- Regular reviews (monthly)
- Dependency updates (weekly)
- Performance optimization (quarterly)
- Breaking change planning (quarterly)
Common Pitfalls and Solutions
| Problem | Solution |
|---|---|
| Too many variants | Document variant strategy, limit combinations |
| Props explosion | Use composition instead of props |
| Poor documentation | Enforce README + stories for each component |
| Hard to update | Design tokens should drive styling |
| Inconsistent naming | Create naming conventions document |
For best practices in component library design, check out the Storybook documentation and the Design Tokens Community Group specifications, which provide comprehensive guidance on structuring and managing design systems. Additionally, explore Carbon Design System by IBM for real-world implementation examples.
FAQ
Q: How many components should I build upfront? A: Build what you need. Core components (Button, Card, Input). Add others as you identify duplicated code.
Q: Should we use CSS-in-JS or CSS modules? A: Either works. We prefer CSS modules for performance and simplicity. CSS-in-JS for dynamic theming.
Q: How do we handle different design requirements for different products? A: Use theming and layout components. Don't duplicate base components.
Q: What about mobile components? A: Build responsive components that work at all sizes. Avoid separate mobile/desktop versions.
Q: How do we encourage adoption? A: Make it easier to use the system than to build new components. Good documentation helps.
Q: Can we use component libraries like Material UI? A: Yes, but consider if you'll customize it. A fully customized library becomes hard to maintain.
Design System Adoption and Change Management
Getting Teams to Use It
The best design system fails if nobody uses it. Here's how we drive adoption:
Show ROI immediately: Demonstrate that using the system saves time. Benchmark: "This button took 3 hours to build from scratch, 5 minutes with our system."
Make discovery easy: Nothing discourages adoption like "I didn't know this component existed." Use searchable documentation, IDE plugins, and component previews.
Support migrations: Don't expect teams to refactor existing code overnight. Provide migration guides and support.
Celebrate adoption: Share metrics about which teams are using the system and what they've shipped faster.
Backward Compatibility and Deprecation
Design systems evolve. Plan migrations carefully:
Code:
// v1.0.0 - Original API
export function Button(props: ButtonPropsV1) { /* ... */ }
// v1.1.0 - Add new variant but keep old API
export function Button(props: ButtonPropsV1 | ButtonPropsV2) {
if ('newProp' in props) {
return <ButtonV2 {...props} />;
}
return <ButtonV1 {...props} />;
}
// v2.0.0 - Remove old API entirely
export function Button(props: ButtonPropsV2) { /* ... */ }
Provide migration tooling:
Code:
// Codemod to update component usage
export function transformButtonProps(props: any) {
return {
variant: props.type || 'primary', // Map 'type' to 'variant'
size: props.size || 'medium',
disabled: props.disabled ?? false
};
}
Measuring Design System Success
Track metrics that matter:
Code:
// Time to implement feature
interface FeatureMetric {
featureName: string;
usedDesignSystem: boolean;
developmentHours: number;
testingHours: number;
}
// Component reuse rate
interface ReuseMetric {
componentName: string;
usageCount: number;
projectsUsing: number;
}
// Quality metrics
interface QualityMetric {
componentName: string;
bugReports: number;
a11yIssues: number;
performanceScore: number;
}
Track these over time. You should see development time decreasing and reuse increasing.
Real-World Design System Examples
Documentation Structure
A well-organized design system has:
Code:
docs/
βββ Getting Started
β βββ Installation
β βββ Quick Start
β βββ Design Principles
βββ Foundations
β βββ Typography
β βββ Color System
β βββ Spacing
β βββ Icons
βββ Components
β βββ Atomic
β β βββ Button
β β βββ Input
β β βββ Label
β βββ Composite
β β βββ Form
β β βββ Modal
β β βββ Navigation
β βββ Patterns
β βββ Authentication Flow
β βββ Data Tables
β βββ Dashboard Layouts
βββ Accessibility
β βββ WCAG Checklist
β βββ Screen Reader Testing
β βββ Keyboard Navigation
βββ Contributing
βββ Component Checklist
βββ Code Review Guide
βββ Release Process
Component Maturity Levels
Not all components are equal. Define maturity levels:
Code:
interface ComponentMetadata {
name: string;
maturity: 'alpha' | 'beta' | 'stable' | 'legacy';
a11yStatus: 'wcag-a' | 'wcag-aa' | 'wcag-aaa';
lastUpdated: Date;
maintainer: string;
usageStats: {
adoptionRate: number;
bugReports: number;
};
}
// Alpha: Experimental, breaking changes expected
// Beta: Mostly stable, minor breaking changes possible
// Stable: Production-ready, semantic versioning
// Legacy: Deprecated, plan migration
Auto-Generated Documentation
Use TypeScript to auto-generate parts of documentation:
Code:
/**
* The Button component is the primary interactive element.
*
* @example
*
tsx
- Click me
Code:
*
* @accessibility
* - Supports keyboard navigation (Space/Enter to activate)
* - Screen reader announces button role and label
* - Respects prefers-reduced-motion
*/
export interface ButtonProps {
/** Visual style of the button */
variant?: 'primary' | 'secondary' | 'tertiary';
/** Size of the button */
size?: 'small' | 'medium' | 'large';
/** Disabled state */
disabled?: boolean;
/** Click handler */
onClick?: () => void;
}
export function Button(props: ButtonProps) { /* ... */ }
Conclusion
A design system is a long-term investment. The first 3-6 months feel slow because you're establishing foundations. But at month 12, you realize new features ship 40% faster because everyone uses the same components. By year two, the system pays for itself through reduced maintenance and faster development.
The teams we work with consistently tell us that investing in a proper design system was one of their best decisions. It doesn't just improve productsβit improves how teams communicate and collaborate. Designers and developers use the same language. New team members get up to speed faster. Quality improves because patterns are proven.
External Resources
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 1000+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement.
Need a Modern Web Application?
From landing pages to complex SaaS platforms β we build it all with Next.js and React.
Free consultation β’ No commitment β’ Response within 24 hours
Need a custom web application built?
We build React and Next.js web applications with Lighthouse β₯90 scores, mobile-first design, and full source code ownership. Senior engineers only β from architecture through deployment.