Skip to content
Back to Design Hub

Design System Documentation with Storybook

Stories, MDX docs, and addon configuration for living component documentation.

Design SystemsIntermediate

Why

Static docs (Notion, README) drift from code within a week. Storybook renders real components with live props, so docs are always in sync.

It also provides isolated dev environment — build a Button without spinning up the whole app.

When

Adopt Storybook for any component library or design system with 5+ components. Skip for app-internal components that are never reused — inline tests suffice.

How

1. Install — npx storybook@latest init, picks framework (React, Vue, Astro partial) 2. Write stories — one .stories.tsx per component, export Default + variants as named exports 3. Add args — use ArgsTable for interactive prop editing, controls auto-generated from TS types 4. MDX docs — import component, write usage prose + live examples in .mdx, set parameters.docs 5. Addons — @storybook/addon-a11y (axe audit), addon-interactions (play function tests) 6. Composition — if multiple design systems, compose them into one Storybook via project IDs 7. Publish — Chromatic for visual regression + hosting, or build static + deploy to GitHub Pages

Example

// Button.stories.tsx — stories + arg types import type { Meta, StoryObj } from "@storybook/react"; import { Button } from "./Button"; const meta: Meta<typeof Button> = { title: "Components/Button", component: Button, tags: ["autodocs"], argTypes: { variant: { control: "select", options: ["primary", "secondary", "ghost"] }, size: { control: "radio", options: ["sm", "md", "lg"] }, disabled: { control: "boolean" }, }, parameters: { docs: { description: { component: "Primary CTA. Use one per view." } }, }, }; export default meta; export const Default: StoryObj<typeof Button> = { args: { children: "Click me", variant: "primary", size: "md" }, }; export const AllVariants: StoryObj<typeof Button> = { render: () => ( <div className="flex gap-2"> <Button variant="primary">Primary</Button> <Button variant="secondary">Secondary</Button> <Button variant="ghost">Ghost</Button> </div> ), }; // Button.mdx — long-form docs with live stories import { Meta, Story } from "@storybook/blocks"; <Meta of={Button} /> # Button Use for the most important action in a view. <Story of={AllVariants} />

Common Problems

  • ⚠Stories don't cover edge cases — only 'happy path' default, error/loading states undocumented
  • ⚠Storybook config drift — main.ts not updated when adding packages, stories fail to load
  • ⚠No a11y addon — components pass visual review but have contrast/keyboard issues
  • ⚠Docs published but stale — no CI build, Storybook shows last manual deploy from 3 months ago

Best Practices

  • ✓Write a story for every state — default, hover, focus, disabled, loading, error
  • ✓Add addon-a11y and fail CI on violations — catches contrast and ARIA issues automatically
  • ✓Build Storybook in CI on every PR — deploy preview to Chromatic, block merge on visual diff
  • ✓Use CSF3 + autodocs — no MDX boilerplate for simple components, MDX only for complex

FAQ

Storybook vs README docs — why Storybook?

READMEs drift from code within a week. Storybook renders real components with live props, so docs are always in sync. It also gives an isolated dev environment.

How many stories should each component have?

One per state: default, hover, focus, disabled, loading, error. Most teams write only the happy-path default and leave edge cases undocumented.

Do I need MDX or is CSF enough?

CSF3 with autodocs covers simple components. Use MDX only for complex components needing long-form usage guidance and embedded live stories.

How do I catch a11y issues in Storybook?

Add @storybook/addon-a11y (runs axe-core on every story) and fail CI on violations. It catches contrast and ARIA issues automatically during dev.

How do I keep Storybook docs from going stale?

Build Storybook in CI on every PR and deploy a preview to Chromatic. Block merge on visual diffs — stale docs become impossible when every PR rebuilds them.