A design system is more than a component library—it's a shared language between design and development. When built well, it accelerates development, ensures consistency, and scales with your organization. Here's how to build one that works.
What Is a Design System?#
Components#
- Buttons, inputs, cards
- Layout primitives
- Navigation patterns
- Data display components
Design Tokens#
- Colors
- Typography
- Spacing
- Shadows
- Animation
Guidelines#
- Usage documentation
- Accessibility standards
- Content patterns
- Interaction patterns
Processes#
- Contribution workflow
- Governance
- Versioning
- Communication
Design Tokens#
Token Structure#
1// tokens/colors.ts
2export const colors = {
3 // Primitives (raw values)
4 primitive: {
5 blue: {
6 50: '#eff6ff',
7 100: '#dbeafe',
8 500: '#3b82f6',
9 900: '#1e3a8a',
10 },
11 gray: {
12 50: '#f9fafb',
13 100: '#f3f4f6',
14 500: '#6b7280',
15 900: '#111827',
16 },
17 },
18
19 // Semantic (purpose-based)
20 semantic: {
21 background: {
22 primary: '{primitive.gray.50}',
23 secondary: '{primitive.gray.100}',
24 inverse: '{primitive.gray.900}',
25 },
26 text: {
27 primary: '{primitive.gray.900}',
28 secondary: '{primitive.gray.500}',
29 inverse: '{primitive.gray.50}',
30 },
31 action: {
32 primary: '{primitive.blue.500}',
33 primaryHover: '{primitive.blue.600}',
34 },
35 },
36};Token Transformation#
1// Using Style Dictionary
2module.exports = {
3 source: ['tokens/**/*.json'],
4 platforms: {
5 css: {
6 transformGroup: 'css',
7 buildPath: 'dist/css/',
8 files: [{
9 destination: 'variables.css',
10 format: 'css/variables',
11 }],
12 },
13 js: {
14 transformGroup: 'js',
15 buildPath: 'dist/js/',
16 files: [{
17 destination: 'tokens.js',
18 format: 'javascript/es6',
19 }],
20 },
21 },
22};Component Architecture#
Component Anatomy#
1// Button component structure
2import { forwardRef } from 'react';
3import { cva, type VariantProps } from 'class-variance-authority';
4import { cn } from '@/lib/utils';
5
6const buttonVariants = cva(
7 // Base styles
8 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50',
9 {
10 variants: {
11 variant: {
12 primary: 'bg-blue-500 text-white hover:bg-blue-600',
13 secondary: 'bg-gray-100 text-gray-900 hover:bg-gray-200',
14 ghost: 'hover:bg-gray-100',
15 destructive: 'bg-red-500 text-white hover:bg-red-600',
16 },
17 size: {
18 sm: 'h-8 px-3 text-sm',
19 md: 'h-10 px-4',
20 lg: 'h-12 px-6 text-lg',
21 },
22 },
23 defaultVariants: {
24 variant: 'primary',
25 size: 'md',
26 },
27 }
28);
29
30interface ButtonProps
31 extends React.ButtonHTMLAttributes<HTMLButtonElement>,
32 VariantProps<typeof buttonVariants> {
33 loading?: boolean;
34}
35
36const Button = forwardRef<HTMLButtonElement, ButtonProps>(
37 ({ className, variant, size, loading, children, disabled, ...props }, ref) => {
38 return (
39 <button
40 ref={ref}
41 className={cn(buttonVariants({ variant, size }), className)}
42 disabled={disabled || loading}
43 {...props}
44 >
45 {loading && <Spinner className="mr-2 h-4 w-4" />}
46 {children}
47 </button>
48 );
49 }
50);
51
52Button.displayName = 'Button';
53
54export { Button, buttonVariants };Composition Patterns#
1// Compound components
2const Card = ({ children }: { children: React.ReactNode }) => (
3 <div className="rounded-lg border bg-white shadow-sm">{children}</div>
4);
5
6Card.Header = ({ children }: { children: React.ReactNode }) => (
7 <div className="border-b px-6 py-4">{children}</div>
8);
9
10Card.Body = ({ children }: { children: React.ReactNode }) => (
11 <div className="px-6 py-4">{children}</div>
12);
13
14Card.Footer = ({ children }: { children: React.ReactNode }) => (
15 <div className="border-t px-6 py-4">{children}</div>
16);
17
18// Usage
19<Card>
20 <Card.Header>Title</Card.Header>
21 <Card.Body>Content</Card.Body>
22 <Card.Footer>Actions</Card.Footer>
23</Card>Documentation#
Component Documentation#
1---
2title: Button
3description: Buttons trigger actions and events.
4---
5
6import { Button } from '@/components/ui/button';
7
8## Usage
9
10<Button>Click me</Button>
11
12## Variants
13
14### Primary (Default)
15Use for primary actions.
16
17<Button variant="primary">Primary</Button>
18
19### Secondary
20Use for secondary actions.
21
22<Button variant="secondary">Secondary</Button>
23
24## Sizes
25
26<div className="flex gap-2 items-center">
27 <Button size="sm">Small</Button>
28 <Button size="md">Medium</Button>
29 <Button size="lg">Large</Button>
30</div>
31
32## Loading State
33
34<Button loading>Saving...</Button>
35
36## Accessibility
37
38- Uses native `<button>` element
39- Supports keyboard navigation
40- Disabled state prevents interaction
41- Loading state announces to screen readers
42
43## Props
44
45| Prop | Type | Default | Description |
46|------|------|---------|-------------|
47| variant | 'primary' \| 'secondary' \| 'ghost' \| 'destructive' | 'primary' | Visual style |
48| size | 'sm' \| 'md' \| 'lg' | 'md' | Button size |
49| loading | boolean | false | Shows loading spinner |
50| disabled | boolean | false | Disables button |Storybook Stories#
1// Button.stories.tsx
2import type { Meta, StoryObj } from '@storybook/react';
3import { Button } from './Button';
4
5const meta: Meta<typeof Button> = {
6 title: 'Components/Button',
7 component: Button,
8 argTypes: {
9 variant: {
10 control: 'select',
11 options: ['primary', 'secondary', 'ghost', 'destructive'],
12 },
13 size: {
14 control: 'select',
15 options: ['sm', 'md', 'lg'],
16 },
17 },
18};
19
20export default meta;
21type Story = StoryObj<typeof Button>;
22
23export const Primary: Story = {
24 args: {
25 children: 'Button',
26 variant: 'primary',
27 },
28};
29
30export const AllVariants: Story = {
31 render: () => (
32 <div className="flex gap-2">
33 <Button variant="primary">Primary</Button>
34 <Button variant="secondary">Secondary</Button>
35 <Button variant="ghost">Ghost</Button>
36 <Button variant="destructive">Destructive</Button>
37 </div>
38 ),
39};
40
41export const Loading: Story = {
42 args: {
43 children: 'Saving...',
44 loading: true,
45 },
46};Accessibility#
Built-In Accessibility#
1// Focus management
2const Dialog = ({ open, onClose, children }) => {
3 const dialogRef = useRef<HTMLDivElement>(null);
4
5 useEffect(() => {
6 if (open) {
7 // Trap focus
8 const focusableElements = dialogRef.current?.querySelectorAll(
9 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
10 );
11 focusableElements?.[0]?.focus();
12 }
13 }, [open]);
14
15 // Handle escape key
16 useEffect(() => {
17 const handleEscape = (e: KeyboardEvent) => {
18 if (e.key === 'Escape' && open) onClose();
19 };
20 window.addEventListener('keydown', handleEscape);
21 return () => window.removeEventListener('keydown', handleEscape);
22 }, [open, onClose]);
23
24 return (
25 <div
26 ref={dialogRef}
27 role="dialog"
28 aria-modal="true"
29 aria-labelledby="dialog-title"
30 >
31 {children}
32 </div>
33 );
34};Accessibility Testing#
1// Automated accessibility testing
2import { axe, toHaveNoViolations } from 'jest-axe';
3
4expect.extend(toHaveNoViolations);
5
6test('Button should have no accessibility violations', async () => {
7 const { container } = render(<Button>Click me</Button>);
8 const results = await axe(container);
9 expect(results).toHaveNoViolations();
10});Versioning and Release#
Semantic Versioning#
MAJOR.MINOR.PATCH
MAJOR: Breaking changes
MINOR: New features (backward compatible)
PATCH: Bug fixes
Example changelog:
## [2.0.0] - 2024-03-15
### Breaking Changes
- Button `type` prop renamed to `variant`
## [1.2.0] - 2024-03-01
### Added
- New `ghost` variant for Button
## [1.1.1] - 2024-02-15
### Fixed
- Button focus ring color in dark mode
Deprecation Strategy#
1// Deprecate with warning
2interface ButtonProps {
3 /** @deprecated Use `variant` instead */
4 type?: 'primary' | 'secondary';
5 variant?: 'primary' | 'secondary';
6}
7
8const Button = ({ type, variant, ...props }) => {
9 if (type) {
10 console.warn(
11 'Button: `type` prop is deprecated. Use `variant` instead.'
12 );
13 }
14
15 const actualVariant = variant || type;
16 // ...
17};Adoption Strategy#
Gradual Migration#
Phase 1: Foundation
- Design tokens
- Basic components (Button, Input)
- Documentation site
Phase 2: Expansion
- More components
- Layout primitives
- Patterns and guidelines
Phase 3: Adoption
- Migration guides
- Team training
- Integration support
Phase 4: Governance
- Contribution process
- Review workflow
- Ownership model
Measuring Success#
Metrics:
- Component adoption rate
- Time to implement features
- Design/dev consistency
- Bug reports per component
- Contribution frequency
Conclusion#
A design system is an investment that compounds over time. Start small with tokens and core components, document thoroughly, and build processes for contribution and governance.
The best design systems grow organically from real needs while maintaining intentional architecture. They evolve through use, feedback, and iteration. Build for your team's actual needs, not an idealized vision.
Success isn't just technical—it's cultural. A design system works when teams trust it, contribute to it, and feel ownership over it.