storybook
Interactive component showcase and design system documentation built with Storybook 8. 5-theme system, density control, accessibility testing. Browse, test, and document all UI components in isolated environments.
Quick Start
Launch Storybook in 30 seconds:
pnpm --filter storybook devOpens at localhost:3700. Browse components, switch themes, test interactions. Skip to Quick Start →
Why storybook?
Components developed in application context. No isolated testing environment. Design inconsistencies across apps. Manual visual testing for regressions. Accessibility issues caught late. No shared component documentation. Theme variations tested manually.
storybook solves this by providing an isolated environment where components are developed, tested, and documented independently from applications.
Production-ready with Storybook 8, 5-theme system, density control, accessibility testing (a11y addon), visual regression (Chromatic), component testing (Vitest + Playwright), and auto-generated documentation.
Use cases
- Component Development — Build and test components in isolation
- Design System Docs — Interactive documentation for all UI components
- Visual Regression — Automated screenshot comparison with Chromatic
- Accessibility Testing — Automated a11y checks with axe-core
- Theme Validation — Test components across 5 different themes
How it works
storybook provides an interactive component browser with live examples:
// stories/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@repo/uni-ui";
const meta: Meta<typeof Button> = {
title: "Components/Button",
component: Button,
tags: ["autodocs"], // Auto-generate docs
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "outline", "ghost"]
}
}
};
export default meta;
type Story = StoryObj<typeof Button>;
// Each export = one story (one component state)
export const Primary: Story = {
args: {
variant: "primary",
children: "Click me"
}
};
export const WithInteraction: Story = {
args: { children: "Test me" },
play: async ({ canvasElement }) => {
// Automated interaction testing
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
await expect(button).toHaveFocus();
}
};Uses Storybook 8 for component isolation, Vite for fast builds, and Chromatic for visual regression testing.
Key features
5-Theme System — Light, Dark, Corporate, Marketing, Admin
Density Control — Comfortable and Compact spacing options
Accessibility Testing — Built-in a11y addon with axe-core
Visual Regression — Chromatic integration for screenshot comparison
Component Testing — Vitest + Playwright for interaction tests
Auto Documentation — Generated from TypeScript types
Quick Start
1. Start the Storybook dev server
pnpm --filter storybook dev2. Browse components
Open http://localhost:3700 and explore the component library.
3. Create a new story
import type { Meta, StoryObj } from "@storybook/react";
import { MyComponent } from "@repo/uni-ui";
const meta: Meta<typeof MyComponent> = {
title: "Components/MyComponent",
component: MyComponent,
tags: ["autodocs"]
};
export default meta;
type Story = StoryObj<typeof MyComponent>;
export const Default: Story = {
args: {
label: "Hello World"
}
};4. Test interactions
import { within, userEvent, expect } from "@storybook/test";
export const WithTest: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole("button"));
}
};That's it! Your component now appears in Storybook with auto-generated documentation, interactive controls, and automated accessibility testing.
Theme Switching
Use the theme toolbar at the top of Storybook to preview your component across all 5 themes instantly.
Technical Details
For Developers: Technical implementation details
Overview
| Property | Value |
|---|---|
| Location | platform/apps/storybook |
| Port | 3700 (development) |
| Framework | Storybook 8.5.0 + Vite 6 |
| Visual Testing | Chromatic |
| Unit Testing | Vitest 4.0 + @vitest/browser-playwright |
| Accessibility | @storybook/addon-a11y (axe-core) |
| Tech Stack | React 19, TypeScript 5, Tailwind CSS 4 |
Project Structure
storybook/
├── stories/ # Component stories
│ ├── Button.stories.tsx # Button component stories
│ ├── Input.stories.tsx # Input component stories
│ ├── Select.stories.tsx # Select component stories
│ ├── Dialog.stories.tsx # Dialog component stories
│ ├── Table.stories.tsx # Table component stories
│ └── ... # 50+ component stories
├── .storybook/ # Storybook configuration
│ ├── main.ts # Core Storybook config
│ ├── manager.ts # UI/Manager customization
│ ├── preview.tsx # Global decorators & parameters
│ ├── test-runner.ts # Test runner configuration
│ ├── vitest.setup.ts # Vitest setup
│ └── utils/ # Helper utilities
│ ├── themes.ts # Theme configuration
│ └── decorators.ts # Custom decorators
├── styles/ # Global styles
│ └── storybook.css # Storybook-specific styles
├── public/ # Static assets
│ ├── images/ # Image assets
│ └── fonts/ # Custom fonts
├── tailwind.config.ts # Tailwind configuration
├── vitest.config.ts # Vitest configuration
├── chromatic.config.json # Chromatic configuration
└── package.json # Dependencies and scriptsTheme System
5 Built-In Themes
| Theme | Primary Color | Description | Use Case |
|---|---|---|---|
| Light | Blue | Default light theme | General purpose, default UI |
| Dark | Blue | Dark mode variant | Dark mode support |
| Corporate | Deep Blue | Professional theme | Business applications |
| Marketing | Pink/Magenta | Vibrant, energetic theme | Marketing pages |
| Admin | Red | Functional, alert theme | Admin interfaces |
Theme Configuration
// .storybook/utils/themes.ts
export const themes = {
light: {
className: "theme-light",
name: "Light",
colors: {
primary: "hsl(221 83% 53%)", // Blue
background: "hsl(0 0% 100%)",
foreground: "hsl(222 47% 11%)"
}
},
dark: {
className: "theme-dark",
name: "Dark",
colors: {
primary: "hsl(221 83% 53%)",
background: "hsl(222 47% 11%)",
foreground: "hsl(210 40% 98%)"
}
},
corporate: {
className: "theme-corporate",
name: "Corporate",
colors: {
primary: "hsl(214 100% 25%)", // Deep Blue
background: "hsl(0 0% 100%)",
foreground: "hsl(222 47% 11%)"
}
},
marketing: {
className: "theme-marketing",
name: "Marketing",
colors: {
primary: "hsl(336 84% 50%)", // Pink/Magenta
background: "hsl(0 0% 100%)",
foreground: "hsl(222 47% 11%)"
}
},
admin: {
className: "theme-admin",
name: "Admin",
colors: {
primary: "hsl(0 84% 60%)", // Red
background: "hsl(0 0% 100%)",
foreground: "hsl(222 47% 11%)"
}
}
};Theme Decorator
// .storybook/preview.tsx
import { withThemeByClassName } from "@storybook/addon-themes";
import { themes } from "./utils/themes";
export const decorators = [
withThemeByClassName({
themes: {
light: themes.light.className,
dark: themes.dark.className,
corporate: themes.corporate.className,
marketing: themes.marketing.className,
admin: themes.admin.className
},
defaultTheme: "light"
})
];Switching Themes in Stories
// stories/Button.stories.tsx
export const AllThemes: Story = {
parameters: {
// Test across all themes
chromatic: {
modes: {
light: { theme: "light" },
dark: { theme: "dark" },
corporate: { theme: "corporate" },
marketing: { theme: "marketing" },
admin: { theme: "admin" }
}
}
}
};Density System
Comfortable vs Compact
// .storybook/preview.tsx
export const globalTypes = {
density: {
description: "Spacing density",
defaultValue: "comfortable",
toolbar: {
title: "Density",
icon: "component",
items: [
{ value: "comfortable", title: "Comfortable" },
{ value: "compact", title: "Compact" },
],
dynamicTitle: true,
},
},
};
// Decorator to apply density class
export const decorators = [
(Story, context) => {
const density = context.globals.density || "comfortable";
return (
<div className={`density-${density}`}>
<Story />
);
},
];CSS Variables:
/* styles/storybook.css */
:root {
/* Comfortable density (default) */
--spacing-xs: 0.25rem;
--spacing-sm: 0.5rem;
--spacing-md: 1rem;
--spacing-lg: 1.5rem;
--spacing-xl: 2rem;
}
.density-compact {
/* Compact density (reduced spacing) */
--spacing-xs: 0.125rem;
--spacing-sm: 0.25rem;
--spacing-md: 0.5rem;
--spacing-lg: 0.75rem;
--spacing-xl: 1rem;
}Writing Stories
Basic Story Structure
// stories/Button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "@repo/uni-ui";
// Meta defines component metadata
const meta: Meta<typeof Button> = {
title: "Components/Button", // Sidebar location
component: Button,
tags: ["autodocs"], // Enable auto-generated docs
parameters: {
layout: "centered", // Story layout
},
argTypes: {
variant: {
control: "select",
options: ["primary", "secondary", "outline", "ghost", "destructive"],
description: "Visual style variant",
},
size: {
control: "select",
options: ["sm", "md", "lg"],
description: "Button size",
},
disabled: {
control: "boolean",
description: "Disabled state",
},
},
};
export default meta;
type Story = StoryObj<typeof Button>;
// Each export = one story
export const Primary: Story = {
args: {
variant: "primary",
children: "Primary Button",
},
};
export const Secondary: Story = {
args: {
variant: "secondary",
children: "Secondary Button",
},
};
export const AllVariants: Story = {
render: () => (
<div className="flex gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Delete</Button>
),
};Story with Interaction Testing
import { within, userEvent, expect } from "@storybook/test";
export const WithInteraction: Story = {
args: {
children: "Click me",
onClick: fn() // Mock function from @storybook/test
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
// Find the button
const button = canvas.getByRole("button");
// Test interaction
await userEvent.click(button);
// Assertions
await expect(args.onClick).toHaveBeenCalled();
await expect(button).toHaveFocus();
}
};Story with Custom Decorator
export const WithBackground: Story = {
args: { children: "Button on background" },
decorators: [
(Story) => (
<div className="p-8 bg-gray-100 rounded">
<Story />
),
],
};Story with Multiple Parameters
export const Documented: Story = {
args: {
variant: "primary",
children: "Documented Button"
},
parameters: {
docs: {
description: {
story: "This button demonstrates primary variant usage."
}
},
a11y: {
config: {
rules: [
{ id: "color-contrast", enabled: true },
{ id: "button-name", enabled: true }
]
}
},
chromatic: {
delay: 300, // Wait 300ms before screenshot
diffThreshold: 0.2 // 20% difference threshold
}
}
};Storybook Configuration
Main Configuration
// .storybook/main.ts
import type { StorybookConfig } from "@storybook/react-vite";
import { join, dirname } from "node:path";
function getAbsolutePath(value: string): string {
return dirname(require.resolve(join(value, "package.json")));
}
const config: StorybookConfig = {
// Story locations
stories: ["../stories/**/*.stories.@(ts|tsx)"],
// Addons
addons: [
getAbsolutePath("@storybook/addon-essentials"),
getAbsolutePath("@storybook/addon-a11y"),
getAbsolutePath("@storybook/addon-themes"),
getAbsolutePath("@storybook/addon-vitest"),
getAbsolutePath("@chromatic-com/storybook"),
getAbsolutePath("@storybook/addon-interactions")
],
// Framework
framework: {
name: getAbsolutePath("@storybook/react-vite"),
options: {}
},
// TypeScript
typescript: {
check: true,
reactDocgen: "react-docgen-typescript",
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
propFilter: (prop) => (prop.parent ? !/node_modules/.test(prop.parent.fileName) : true)
}
},
// Vite configuration
viteFinal: async (config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve?.alias,
"@": join(__dirname, "../")
}
}
};
},
// Static directories
staticDirs: ["../public"],
// Documentation
docs: {
autodocs: "tag" // Auto-generate docs for stories with 'autodocs' tag
}
};
export default config;Preview Configuration
// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { withThemeByClassName } from "@storybook/addon-themes";
import "@repo/uni-ui/theme.css"; // Tailwind CSS 4 theme
const preview: Preview = {
// Global parameters
parameters: {
// Controls
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
expanded: true, // Expand controls panel by default
},
// Actions
actions: {
argTypesRegex: "^on[A-Z].*", // Auto-detect event handlers
},
// Backgrounds
backgrounds: {
default: "light",
values: [
{ name: "light", value: "#ffffff" },
{ name: "dark", value: "#1a1a1a" },
{ name: "gray", value: "#f5f5f5" },
],
},
// Layout
layout: "centered", // Default layout
// Documentation
docs: {
toc: true, // Show table of contents
source: {
type: "dynamic", // Show source code
},
},
// Viewport
viewport: {
viewports: {
mobile: {
name: "Mobile",
styles: { width: "375px", height: "667px" },
},
tablet: {
name: "Tablet",
styles: { width: "768px", height: "1024px" },
},
desktop: {
name: "Desktop",
styles: { width: "1920px", height: "1080px" },
},
},
},
},
// Global types (toolbar items)
globalTypes: {
theme: {
description: "Global theme for components",
defaultValue: "light",
toolbar: {
title: "Theme",
icon: "paintbrush",
items: [
{ value: "light", title: "Light", icon: "circlehollow" },
{ value: "dark", title: "Dark", icon: "circle" },
{ value: "corporate", title: "Corporate", icon: "diamond" },
{ value: "marketing", title: "Marketing", icon: "heart" },
{ value: "admin", title: "Admin", icon: "admin" },
],
dynamicTitle: true,
},
},
density: {
description: "Spacing density",
defaultValue: "comfortable",
toolbar: {
title: "Density",
icon: "component",
items: [
{ value: "comfortable", title: "Comfortable" },
{ value: "compact", title: "Compact" },
],
dynamicTitle: true,
},
},
},
// Global decorators
decorators: [
// Theme decorator
withThemeByClassName({
themes: {
light: "theme-light",
dark: "theme-dark",
corporate: "theme-corporate",
marketing: "theme-marketing",
admin: "theme-admin",
},
defaultTheme: "light",
}),
// Density decorator
(Story, context) => {
const density = context.globals.density || "comfortable";
return (
<div className={`density-${density}`}>
<Story />
);
},
],
};
export default preview;Manager Configuration
// .storybook/manager.ts
import { addons } from "@storybook/manager-api";
import { themes } from "@storybook/theming";
addons.setConfig({
theme: themes.light,
sidebar: {
showRoots: true,
collapsedRoots: ["other"]
},
toolbar: {
title: { hidden: false },
zoom: { hidden: false },
eject: { hidden: false },
copy: { hidden: false },
fullscreen: { hidden: false }
}
});Accessibility Testing
A11y Addon Configuration
// .storybook/preview.tsx
parameters: {
a11y: {
// Default a11y configuration
element: "#storybook-root",
config: {
rules: [
{
id: "color-contrast",
enabled: true,
reviewOnFail: true,
},
{
id: "button-name",
enabled: true,
},
{
id: "label",
enabled: true,
},
{
id: "link-name",
enabled: true,
},
],
},
options: {
runOnly: {
type: "tag",
values: ["wcag2a", "wcag2aa", "wcag21aa"],
},
},
},
}Per-Story A11y Configuration
export const AccessibleButton: Story = {
args: {
children: "Accessible Button",
"aria-label": "Submit form"
},
parameters: {
a11y: {
config: {
rules: [
// Highlight specific rules
{ id: "color-contrast", enabled: true }
]
}
}
}
};Manual A11y Testing
import { expect } from "@storybook/test";
import { axe, toHaveNoViolations } from "jest-axe";
expect.extend(toHaveNoViolations);
export const ManualA11yTest: Story = {
play: async ({ canvasElement }) => {
const results = await axe(canvasElement);
expect(results).toHaveNoViolations();
}
};Visual Regression Testing
Chromatic Configuration
// chromatic.config.json
{
"projectId": "your-project-id",
"buildScriptName": "build-storybook",
"exitZeroOnChanges": true,
"exitOnceUploaded": true,
"onlyChanged": true,
"externals": ["public/**"],
"debug": false
}Running Chromatic
# Run Chromatic visual tests
pnpm --filter storybook chromatic
# With specific options
npx chromatic \
--project-token=$CHROMATIC_PROJECT_TOKEN \
--only-changed \
--auto-accept-changes mainChromatic-Specific Story Configuration
export const ChromaticTest: Story = {
parameters: {
chromatic: {
delay: 500, // Wait 500ms before screenshot
diffThreshold: 0.3, // 30% difference threshold
pauseAnimationAtEnd: true, // Pause animations
viewports: [375, 768, 1920], // Test multiple viewports
modes: {
light: { theme: "light" },
dark: { theme: "dark" }
}
}
}
};Disabling Chromatic for Specific Stories
export const SkipChromatic: Story = {
parameters: {
chromatic: { disableSnapshot: true }
}
};Component Testing
Vitest Configuration
// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: [".storybook/vitest.setup.ts"],
browser: {
enabled: true,
name: "playwright",
provider: "playwright",
headless: true
},
coverage: {
provider: "v8",
reporter: ["text", "json", "html"],
exclude: ["**/*.stories.tsx", "**/node_modules/**"]
}
}
});Test Runner Configuration
// .storybook/test-runner.ts
import type { TestRunnerConfig } from "@storybook/test-runner";
const config: TestRunnerConfig = {
async postVisit(page, context) {
// Run accessibility tests
const elementHandler = await page.$("#storybook-root");
const innerHTML = await elementHandler?.innerHTML();
expect(innerHTML).toBeTruthy();
}
};
export default config;Running Tests
# Run interaction tests
pnpm --filter storybook test-storybook
# Run with coverage
pnpm --filter storybook coverage:collect
# Run specific story
pnpm --filter storybook test-storybook --stories-filter="Button/*"Environment Variables
# Storybook Server
STORYBOOK_PORT=3700
# Chromatic
CHROMATIC_PROJECT_TOKEN="chpt_..."
CHROMATIC_APP_CODE="..."
# Feature Flags
NEXT_PUBLIC_ENABLE_STORYBOOK_PREVIEW="true"Scripts Reference
{
"scripts": {
"dev": "storybook dev -p 3700",
"build": "storybook build",
"build-storybook": "storybook build",
"preview": "http-server storybook-static -p 3700",
"test-storybook": "test-storybook",
"coverage:collect": "test-storybook --coverage",
"chromatic": "chromatic --exit-zero-on-changes",
"lint": "eslint stories/**/*.tsx",
"typecheck": "tsc --noEmit"
}
}Command Descriptions:
dev— Start Storybook dev server on port 3700build— Build static Storybook sitepreview— Preview built Storybook locallytest-storybook— Run interaction testscoverage:collect— Run tests with coverage reportchromatic— Run visual regression testslint— Lint story filestypecheck— Type check TypeScript
Deployment
Static Build
# Build Storybook for deployment
pnpm --filter storybook build
# Output directory: storybook-static/
# Can be deployed to any static hostingChromatic Hosting
Chromatic automatically hosts your Storybook and provides shareable URLs:
# Deploy to Chromatic
npx chromatic --project-token=$CHROMATIC_PROJECT_TOKEN
# Output:
# ✔ Storybook published to https://your-project.chromatic.com
# ✔ Visual tests passed: 0 changes detectedCloudflare Pages Deployment
# Deploy to Cloudflare Pages
pnpm --filter storybook build
wrangler pages deploy storybook-static --project-name=storybookVercel Deployment
# Deploy to Vercel
pnpm --filter storybook build
vercel --prod storybook-staticBest Practices
Story Organization
stories/
├── Components/ # UI components
│ ├── Button.stories.tsx
│ ├── Input.stories.tsx
│ └── ...
├── Forms/ # Form components
│ ├── FormField.stories.tsx
│ ├── FormLabel.stories.tsx
│ └── ...
├── Layout/ # Layout components
│ ├── Container.stories.tsx
│ ├── Grid.stories.tsx
│ └── ...
└── Examples/ # Full page examples
├── LoginPage.stories.tsx
└── DashboardPage.stories.tsxNaming Conventions
- Story files:
ComponentName.stories.tsx - Story exports:
PascalCase(e.g.,Primary,WithIcon) - Story titles:
Category/ComponentName(e.g.,Components/Button)
Documentation
// Add JSDoc comments for auto-generated docs
export interface ButtonProps {
/** Visual style variant */
variant?: "primary" | "secondary" | "outline";
/** Button size */
size?: "sm" | "md" | "lg";
/** Disabled state */
disabled?: boolean;
/** Click handler */
onClick?: () => void;
}Interaction Testing
// Test user interactions comprehensively
export const CompleteInteraction: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// 1. Find elements
const input = canvas.getByRole("textbox");
const button = canvas.getByRole("button");
// 2. Perform actions
await userEvent.type(input, "test@example.com");
await userEvent.click(button);
// 3. Assert results
await expect(input).toHaveValue("test@example.com");
await expect(button).toBeDisabled();
}
};Troubleshooting
Storybook Not Loading
Issue: Storybook fails to start with build errors.
Solution: Clear cache and rebuild:
pnpm --filter storybook clean
rm -rf node_modules/.cache
pnpm --filter storybook devTheme Not Switching
Issue: Theme toolbar doesn't change component appearance.
Solution: Verify theme decorator is applied:
// .storybook/preview.tsx
import { withThemeByClassName } from "@storybook/addon-themes";
export const decorators = [
withThemeByClassName({
themes: {
light: "theme-light",
dark: "theme-dark"
},
defaultTheme: "light"
})
];Chromatic Visual Diffs
Issue: Chromatic shows false positive visual differences.
Solution: Increase diff threshold or add delay:
export const Story: Story = {
parameters: {
chromatic: {
delay: 1000, // Wait 1s for animations
diffThreshold: 0.5 // Increase threshold to 50%
}
}
};A11y Violations
Issue: Accessibility tests failing.
Solution: Fix ARIA labels and color contrast:
<Button
aria-label="Submit form" // Add ARIA label
className="bg-blue-600 text-white" // Ensure sufficient contrast
>
Submit
</Button>Related Documentation
- @repo/uni-ui — UI components showcased in Storybook
- docs — Documentation site using some components