Back to Blog
Design SystemsUI ComponentsFrontendArchitecture

Building Design Systems: From Components to Culture

Create a design system that scales. Learn component architecture, documentation, governance, and adoption strategies.

B
Bootspring Team
Product
August 22, 2025
6 min read

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.

Share this article

Help spread the word about Bootspring