Building a Design System with React, TypeScript, and Storybook | SoniNow Blog

Limited TimeLearn More

design systemreacttypescriptstorybookui

Building a Design System with React, TypeScript, and Storybook

Published

2026-06-23

Read Time

4 mins

Building a Design System with React, TypeScript, and Storybook

A design system is the single source of truth for your product's UI. It ensures visual consistency across teams, accelerates development, and reduces the gap between design and implementation. Building one with React, TypeScript, and Storybook gives you a type-safe, documented, and testable component library.

Component Architecture: Atomic Design Principles

Organize components by complexity using the atomic design hierarchy — atoms, molecules, organisms, templates, and pages. This structure makes it clear where each component belongs and how complex it should be.

// atoms/Button/Button.tsx — the smallest reusable unit
import { type ButtonHTMLAttributes, forwardRef } from 'react'
import { type VariantProps, cva } from 'class-variance-authority'

const buttonVariants = cva(
  'inline-flex items-center justify-center rounded-md 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-brand text-white hover:bg-brand-dark',
        secondary: 'bg-surface text-foreground border hover:bg-muted',
        ghost: 'hover:bg-muted text-foreground',
        destructive: 'bg-red-600 text-white hover:bg-red-700',
      },
      size: {
        sm: 'h-8 px-3 text-xs',
        md: 'h-10 px-4 text-sm',
        lg: 'h-12 px-6 text-base',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
)

export interface ButtonProps
  extends ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {}

export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, ...props }, ref) => {
    return (
      <button
        className={buttonVariants({ variant, size, className })}
        ref={ref}
        {...props}
      />
    )
  }
)
Button.displayName = 'Button'

The class-variance-authority library types the variant props, so consumers get autocomplete for variant and size. The forwardRef pattern ensures the component works with form libraries and tooltips.

Theming with CSS Custom Properties

Design tokens belong in CSS custom properties, not in JavaScript. This allows theming without JavaScript overhead and enables runtime theme switching.

/* tokens.css */
:root {
  --color-brand: #6c5ce7;
  --color-brand-dark: #5a4bd1;
  --color-foreground: #0f172a;
  --color-background: #ffffff;
  --color-muted: #f1f5f9;
  --color-surface: #f8fafc;
  --color-border: #e2e8f0;
  --radius-sm: 0.375rem;
  --radius-md: 0.5rem;
  --radius-lg: 0.75rem;
  --font-sans: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
  --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}

.dark {
  --color-foreground: #f8fafc;
  --color-background: #0f172a;
  --color-muted: #1e293b;
  --color-surface: #1e293b;
  --color-border: #334155;
}
// Components reference tokens as CSS variables, not raw values
const cardVariants = cva('rounded-lg border bg-[var(--color-surface)] p-4 shadow-sm')

This approach lets you create a dark theme, high-contrast theme, or brand-specific theme by changing CSS variables — no JavaScript state management needed.

Storybook Documentation and Testing

Storybook serves as both a development environment and living documentation. Each component gets stories covering its states, variants, and edge cases.

// Button.stories.ts
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './Button'

const meta: Meta<typeof Button> = {
  title: 'Atoms/Button',
  component: Button,
  tags: ['autodocs'],
  argTypes: {
    variant: {
      control: 'select',
      options: ['primary', 'secondary', 'ghost', 'destructive'],
    },
    size: { control: 'select', options: ['sm', 'md', 'lg'] },
    disabled: { control: 'boolean' },
  },
}

export default meta

type Story = StoryObj<typeof Button>

export const Primary: Story = {
  args: { variant: 'primary', children: 'Click me' },
}

export const Secondary: Story = {
  args: { variant: 'secondary', children: 'Cancel' },
}

export const Destructive: Story = {
  args: { variant: 'destructive', children: 'Delete' },
}

export const Disabled: Story = {
  args: { disabled: true, children: 'Disabled' },
}

export const WithIcon: Story = {
  args: { children: '← Back', variant: 'ghost' },
}

Add an accessibility addon (@storybook/addon-a11y) to catch contrast ratio issues, missing ARIA labels, and keyboard navigation problems during development.

Package Structure and Distribution

Publish your design system as a private npm package. Structure the repository so consuming projects only import what they need.

packages/ui/
├── src/
│   ├── atoms/
│   │   ├── Button/
│   │   │   ├── Button.tsx
│   │   │   └── Button.stories.ts
│   │   └── Icon/
│   │       ├── Icon.tsx
│   │       └── Icon.stories.ts
│   ├── molecules/
│   │   ├── Card/
│   │   └── FormField/
│   ├── organisms/
│   │   ├── Header/
│   │   └── DataTable/
│   ├── tokens/
│   │   ├── colors.css
│   │   ├── typography.css
│   │   └── spacing.css
│   └── index.ts
├── package.json
└── tsconfig.json

Using exports in package.json enables deep imports — consumers import only the components they use, keeping bundle sizes small.

Building a design system is an investment that compounds over time — every new product or feature benefits from the existing component library. At SoniNow, we help teams design and implement scalable design systems with React, TypeScript, and Storybook that accelerate development and ensure visual consistency.

Ready to build your design system? Contact SoniNow and let us help you create a component library that scales with your product.