Build a Storybook component library that serves as living documentation for designers and developers — write stories for all component states, integrate Chromatic for visual regression testing, and publish Storybook as a shared team resource.
Storybook is a development tool for building UI components in isolation and documenting them with interactive stories. For design-development collaboration, Storybook bridges the gap between Figma designs and working code: designers can see every component state without needing a full application running, and developers can verify implementations match designs without navigating complex page flows.
Combined with Chromatic for visual regression testing, Storybook becomes an automated quality gate: every PR that changes a component is compared against a visual baseline, and any unintended changes are flagged before reaching design review.
This lesson covers building a production-quality Storybook that serves as the team's living design system documentation.
Component Story Format 3 (CSF3) is the current standard for Storybook stories. Stories are exported functions that describe component states.
CSF3 story structure:
1// Button.stories.tsx
2import type { Meta, StoryObj } from '@storybook/react';
3import { Button } from './Button';
4
5// Meta: configures the stories file
6const meta = {
7 title: 'Components/Button', // sidebar path
8 component: Button, // the component being documented
9 parameters: {
10 layout: 'centered', // story canvas layout
11 docs: {
12 description: {
13 component: 'Primary interactive element. Use for the main action on any view. Maximum one Primary button per screen.'
14 }
15 }
16 },
17 argTypes: {
18 variant: {
19 control: 'select',
20 options: ['primary', 'secondary', 'ghost', 'danger'],
21 description: 'Visual style variant'
22 },
23 size: {
24 control: 'radio',
25 options: ['sm', 'md', 'lg'],
26 },
27 loading: { control: 'boolean' },
28 disabled: { control: 'boolean' },
29 children: { control: 'text' },
30 },
31} satisfies Meta<typeof Button>;
32
33export default meta;
34type Story = StoryObj<typeof meta>;
35
36// Primary story — the most common use case
37export const Primary: Story = {
38 args: {
39 variant: 'primary',
40 size: 'md',
41 children: 'Enroll now',
42 },
43};
44
45// Hover story — force hover state for Chromatic
46export const PrimaryHover: Story = {
47 args: { ...Primary.args },
48 parameters: {
49 pseudo: { hover: true } // requires storybook-addon-pseudo-states
50 },
51};
52
53// All variants in one story for at-a-glance review
54export const AllVariants: Story = {
55 render: () => (
56 <div style={{ display: 'flex', gap: '1rem', flexWrap: 'wrap' }}>
57 <Button variant="primary">Primary</Button>
58 <Button variant="secondary">Secondary</Button>
59 <Button variant="ghost">Ghost</Button>
60 <Button variant="danger">Delete</Button>
61 </div>
62 )
63};
64
65export const AllSizes: Story = {
66 render: () => (
67 <div style={{ display: 'flex', alignItems: 'center', gap: '1rem' }}>
68 <Button variant="primary" size="sm">Small</Button>
69 <Button variant="primary" size="md">Medium</Button>
70 <Button variant="primary" size="lg">Large</Button>
71 </div>
72 )
73};
74
75export const Loading: Story = {
76 args: { ...Primary.args, loading: true }
77};
78
79export const Disabled: Story = {
80 args: { ...Primary.args, disabled: true }
81};Story naming convention:
Storybook addons extend the default interface with tools for accessibility, responsive testing, design token display, and Figma embedding.
Essential addons for design-development collaboration:
1# Install all essential addons
2npm install -D \
3 @storybook/addon-controls # interactive prop controls
4 @storybook/addon-actions # event logging
5 @storybook/addon-viewport # responsive testing
6 @storybook/addon-a11y # accessibility checking
7 @storybook/addon-docs # documentation pages
8 storybook-addon-pseudo-states # CSS pseudo-class simulation
9 @storybook/addon-designs # Figma frame embedding.storybook/main.ts configuration:
1import type { StorybookConfig } from '@storybook/nextjs';
2
3const config: StorybookConfig = {
4 stories: ['../src/**/*.stories.@(js|jsx|ts|tsx|mdx)'],
5 addons: [
6 '@storybook/addon-links',
7 '@storybook/addon-essentials', // includes controls, actions, docs, viewport
8 '@storybook/addon-a11y',
9 'storybook-addon-pseudo-states',
10 '@storybook/addon-designs',
11 ],
12 framework: '@storybook/nextjs',
13};
14
15export default config;Controls addon — interactive prop editing:
Accessibility addon — axe integration:
Viewport addon — responsive testing:
1// .storybook/preview.ts
2export const parameters = {
3 viewport: {
4 viewports: {
5 mobile: { name: 'Mobile (375px)', styles: { width: '375px', height: '812px' } },
6 tablet: { name: 'Tablet (768px)', styles: { width: '768px', height: '1024px' } },
7 desktop: { name: 'Desktop (1440px)', styles: { width: '1440px', height: '900px' } },
8 },
9 defaultViewport: 'desktop',
10 },
11};Designs addon — Figma frame embedding:
1export const Primary: Story = {
2 args: { ... },
3 parameters: {
4 design: {
5 type: 'figma',
6 url: 'https://figma.com/file/xxx?node-id=...', // specific component node
7 },
8 },
9};Designers visiting Storybook see both the live component and the Figma design side-by-side — the ultimate design-implementation comparison tool.
Chromatic runs Storybook stories in a cloud browser, takes screenshots, and compares them to a visual baseline. Any visual change triggers a review workflow.
Chromatic setup:
1npm install -D chromatic
2
3# First run: creates initial baseline snapshots
4npx chromatic --project-token=YOUR_TOKEN
5
6# Set up GitHub Action for PR-triggered testing.github/workflows/chromatic.yml:
1name: Chromatic Visual Tests
2on:
3 push:
4 branches: ['**']
5
6jobs:
7 chromatic:
8 runs-on: ubuntu-latest
9 steps:
10 - uses: actions/checkout@v4
11 with:
12 fetch-depth: 0 # Chromatic needs full history for branching
13 - uses: actions/setup-node@v4
14 with:
15 node-version: '20'
16 cache: 'npm'
17 - run: npm ci
18 - name: Run Chromatic
19 uses: chromaui/action@latest
20 with:
21 projectToken: ${{ secrets.CHROMATIC_TOKEN }}
22 exitZeroOnChanges: true # PR passes even if changes detected
23 # exitOnceUploaded: true # don't block CI, review asyncChromatic review workflow:
Optimizing Chromatic for speed and cost:
1# Only run Chromatic on changed stories (story file diffs)
2npx chromatic --only-changed
3
4# Skip CI stories (generated/data-heavy stories)
5# Add to story parameters:
6parameters: {
7 chromatic: { disableSnapshot: true } # skip this story
8}
9
10# Set story-level delay for animations
11parameters: {
12 chromatic: { delay: 300 } # wait 300ms before screenshot
13}Chromatic story configuration for reliable snapshots:
1export const ButtonHoverTest: Story = {
2 args: { variant: 'primary', children: 'Test' },
3 parameters: {
4 pseudo: { hover: true }, // forces hover state
5 chromatic: { delay: 200 } // wait for transition to complete
6 },
7};A publicly accessible Storybook URL gives designers access to the living component library without running the development environment.
Publishing to Chromatic (free tier available):
1# Chromatic automatically publishes Storybook on every run
2npx chromatic --project-token=YOUR_TOKEN
3# Output: Published Storybook: https://xxx.chromatic.comPublishing to Vercel:
1# Build Storybook as static site
2npm run build-storybook # output: storybook-static/
3
4# Deploy to Vercel
5npx vercel deploy ./storybook-static --prod
6# Output: https://storybook.devbootstrap.comGitHub Pages deployment:
1# .github/workflows/storybook.yml
2name: Deploy Storybook
3on:
4 push:
5 branches: ['master']
6jobs:
7 deploy:
8 steps:
9 - run: npm run build-storybook -- --output-dir ./storybook-static
10 - uses: peaceiris/actions-gh-pages@v3
11 with:
12 github_token: ${{ secrets.GITHUB_TOKEN }}
13 publish_dir: ./storybook-static
14 destination_dir: storybookDesigner access to published Storybook:
Storybook as the single source of truth for design system status:
1Figma → design system 'source of truth' (what it should be)
2Storybook → implementation 'source of truth' (what it is)
3
4When Figma and Storybook diverge:
5 - Designer: flag for design QA
6 - Developer: update implementation to match Figma
7 - Or: update Figma to match implementation (if implementation is better)
8
9Chromatic: catches divergence automatically for any CSS change
10Designs addon: visual comparison for manual reviewStorybook URL structure for team links:
1https://storybook.devbootstrap.com/?path=/story/components-button--primary
2 ^component ^story
3
4Share links to specific stories:
5- Button default: ?path=/story/components-button--primary
6- Button all variants: ?path=/story/components-button--all-variants
7- Card hover state: ?path=/story/course-coursecard--hoverSetting up Storybook with all addons, writing comprehensive stories for the Button and CourseCard components, and integrating Chromatic for automated visual QA.
1// src/components/ui/Button.stories.tsx — Production-ready stories
2import type { Meta, StoryObj } from '@storybook/react';
3import { userEvent, within, expect } from '@storybook/test';
4import { Button } from './Button';
5
6const meta = {
7 title: 'Components/Button',
8 component: Button,
9 parameters: {
10 layout: 'centered',
11 a11y: { element: '#storybook-root' }, // accessibility check scope
12 design: {
13 type: 'figma',
14 url: 'https://figma.com/file/xxx?node-id=button-component',
15 },
16 },
17 tags: ['autodocs'], // generates documentation page automatically
18 argTypes: {
19 variant: {
20 control: 'select',
21 options: ['primary', 'secondary', 'ghost', 'danger', 'link'],
22 description: 'Visual style. Primary: one per view. Danger: destructive actions only.',
23 },
24 size: { control: 'radio', options: ['sm', 'md', 'lg'] },
25 loading: { control: 'boolean', description: 'Shows spinner, hides label' },
26 disabled: { control: 'boolean' },
27 fullWidth: { control: 'boolean', description: 'width: 100%, use in forms' },
28 children: { control: 'text' },
29 },
30} satisfies Meta<typeof Button>;
31
32export default meta;
33type Story = StoryObj<typeof meta>;
34
35// Core stories
36export const Primary: Story = {
37 args: { variant: 'primary', size: 'md', children: 'Enroll now' },
38};
39
40export const Secondary: Story = {
41 args: { variant: 'secondary', size: 'md', children: 'Preview course' },
42};
43
44export const Ghost: Story = {
45 args: { variant: 'ghost', size: 'md', children: 'Cancel' },
46};
47
48export const Danger: Story = {
49 args: { variant: 'danger', size: 'md', children: 'Delete account' },
50};
51
52// State stories (for Chromatic regression)
53export const PrimaryHover: Story = {
54 args: Primary.args,
55 parameters: {
56 pseudo: { hover: true },
57 chromatic: { delay: 200 }, // wait for hover transition
58 },
59};
60
61export const PrimaryFocus: Story = {
62 args: Primary.args,
63 parameters: { pseudo: { focusVisible: true } },
64};
65
66export const Loading: Story = {
67 args: { ...Primary.args, loading: true },
68};
69
70export const Disabled: Story = {
71 args: { ...Primary.args, disabled: true },
72};
73
74// Overview stories
75export const AllVariants: Story = {
76 render: () => (
77 <div style={{ display: 'flex', gap: '0.75rem', flexWrap: 'wrap', padding: '1rem' }}>
78 <Button variant="primary">Primary</Button>
79 <Button variant="secondary">Secondary</Button>
80 <Button variant="ghost">Ghost</Button>
81 <Button variant="danger">Danger</Button>
82 </div>
83 ),
84 parameters: { chromatic: { delay: 100 } },
85};
86
87export const AllSizes: Story = {
88 render: () => (
89 <div style={{ display: 'flex', alignItems: 'center', gap: '0.75rem' }}>
90 <Button size="sm">Small</Button>
91 <Button size="md">Medium</Button>
92 <Button size="lg">Large</Button>
93 </div>
94 ),
95};
96
97// Interaction test story
98export const ClickInteraction: Story = {
99 args: Primary.args,
100 play: async ({ canvasElement, args }) => {
101 const canvas = within(canvasElement);
102 const button = canvas.getByRole('button', { name: /enroll now/i });
103 await userEvent.click(button);
104 // Note: we verify the button was clicked, not what happens after
105 // (that is the responsibility of the parent component's story)
106 },
107};
108
109// .storybook/preview.ts — Global configuration
110export const parameters = {
111 backgrounds: {
112 default: 'light',
113 values: [
114 { name: 'light', value: '#ffffff' },
115 { name: 'surface', value: '#f9fafb' },
116 { name: 'dark', value: '#111827' },
117 ],
118 },
119 viewport: {
120 viewports: {
121 mobile: { name: 'Mobile 375px', styles: { width: '375px', height: '812px' } },
122 tablet: { name: 'Tablet 768px', styles: { width: '768px', height: '1024px' } },
123 desktop: { name: 'Desktop 1440px', styles: { width: '1440px', height: '900px' } },
124 },
125 },
126};The story file covers every Button state needed for Chromatic visual regression: Default, Hover (via pseudo-states addon), Focus, Loading, Disabled, plus overview stories showing all variants and sizes. The PrimaryHover story uses pseudo: { hover: true } and chromatic: { delay: 200 } — the delay ensures the 150ms transition completes before Chromatic takes its screenshot. The ClickInteraction story demonstrates the Storybook test interaction API for verifying the button is interactive. The global preview.ts configuration provides background options (important for testing ghost buttons on different backgrounds) and consistent viewport presets that match the team's Figma artboard sizes.
The next lesson covers Remote Design Collaboration — asynchronous workflows, time-zone coordination, and tools for distributed design-development teams across multiple geographic locations.
Your progress is saved in this browser, on this device only.