Storybook 8 is the most significant release the project has seen in years. It brings a rebuilt Vite-first architecture, a completely revamped testing story with built-in interaction tests, and first-class support for React Server Components. Combined with Next.js 15 and its App Router, you get a powerful workflow for developing, documenting, and testing UI components in total isolation — before they ever touch a page.
In this tutorial, you will set up Storybook 8 inside a fresh Next.js 15 project, write stories using the CSF3 format, test component interactions, run accessibility checks, and generate polished documentation automatically. By the end, your component workflow will be fully isolated, reproducible, and ready for CI.
Prerequisites
Before starting, ensure you have:
- Node.js 20 or newer installed
- Familiarity with React and TypeScript basics
- A working Next.js project (we'll create one from scratch)
- npm or pnpm as your package manager
What You'll Build
You will build a small design system component — a Button — and a more complex ProductCard component. Along the way you will:
- Install and configure Storybook 8 with the Next.js framework adapter
- Write CSF3 stories with typed args and auto-generated controls
- Add decorators to mock Next.js-specific APIs (Image, Link, Router)
- Write interaction tests that run inside Storybook and in CI
- Enable the accessibility (a11y) addon for WCAG compliance checks
- Auto-generate MDX documentation with
autodocs - Integrate with Chromatic for visual regression testing
Step 1: Create a Next.js 15 Project
Start by scaffolding a new Next.js 15 application with TypeScript and Tailwind CSS:
npx create-next-app@latest my-design-system \
--typescript \
--tailwind \
--eslint \
--app \
--src-dir \
--import-alias "@/*"
cd my-design-systemAccept all defaults. This gives you a clean App Router project under src/app/.
Step 2: Initialize Storybook 8
Storybook 8 ships with a smart init command that detects your framework automatically:
npx storybook@latest initThe CLI will:
- Detect Next.js and install
@storybook/nextjs(the official framework adapter) - Add required dependencies:
storybook,@storybook/react,@storybook/addon-essentials - Generate a
.storybook/config directory - Create example story files in
src/stories/ - Add
storybookandbuild-storybookscripts topackage.json
Once the install completes, run the dev server:
npm run storybookOpen http://localhost:6006 in your browser. You should see the Storybook UI with the sample stories.
Step 3: Understand the Configuration Files
Storybook places its config in .storybook/. Two files matter most.
.storybook/main.ts — framework config and addons:
import type { StorybookConfig } from "@storybook/nextjs";
const config: StorybookConfig = {
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
addons: [
"@storybook/addon-essentials",
"@storybook/addon-interactions",
"@storybook/addon-a11y",
],
framework: {
name: "@storybook/nextjs",
options: {},
},
staticDirs: ["../public"],
};
export default config;.storybook/preview.ts — global decorators and parameters:
import type { Preview } from "@storybook/react";
import "../src/app/globals.css";
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;The staticDirs entry tells Storybook to serve files from public/, so next/image can resolve local images correctly.
Step 4: Create a Button Component
Add a reusable Button component at src/components/Button.tsx:
import { ButtonHTMLAttributes } from "react";
import { cva, type VariantProps } 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 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-blue-600 text-white hover:bg-blue-700",
destructive: "bg-red-600 text-white hover:bg-red-700",
outline: "border border-gray-300 bg-white hover:bg-gray-50 text-gray-900",
ghost: "hover:bg-gray-100 text-gray-900",
},
size: {
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-base",
lg: "h-12 px-6 text-lg",
},
},
defaultVariants: {
variant: "default",
size: "md",
},
}
);
export interface ButtonProps
extends ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
label: string;
loading?: boolean;
}
export function Button({ label, variant, size, loading, className, ...props }: ButtonProps) {
return (
<button
className={buttonVariants({ variant, size, className })}
disabled={loading || props.disabled}
{...props}
>
{loading && (
<svg className="mr-2 h-4 w-4 animate-spin" viewBox="0 0 24 24" fill="none">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z" />
</svg>
)}
{label}
</button>
);
}Install class-variance-authority if you haven't already:
npm install class-variance-authorityStep 5: Write Your First Story with CSF3
Create src/components/Button.stories.tsx:
import type { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import { Button } from "./Button";
const meta = {
title: "Design System/Button",
component: Button,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
argTypes: {
variant: {
control: "select",
options: ["default", "destructive", "outline", "ghost"],
},
size: {
control: "select",
options: ["sm", "md", "lg"],
},
loading: { control: "boolean" },
disabled: { control: "boolean" },
},
args: {
onClick: fn(),
label: "Click me",
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
variant: "default",
size: "md",
},
};
export const Destructive: Story = {
args: {
variant: "destructive",
label: "Delete item",
},
};
export const Outline: Story = {
args: {
variant: "outline",
label: "Cancel",
},
};
export const Loading: Story = {
args: {
loading: true,
label: "Saving...",
},
};
export const AllSizes: Story = {
render: () => (
<div className="flex items-center gap-4">
<Button label="Small" size="sm" />
<Button label="Medium" size="md" />
<Button label="Large" size="lg" />
</div>
),
};The satisfies Meta<typeof Button> pattern (CSF3) gives you full type inference on args while keeping the meta object extensible.
Open Storybook in your browser — you should see the Button stories under Design System/Button with auto-generated controls for every prop.
Step 6: Using Args and the Controls Panel
In CSF3, args are the live props passed to your component. The Controls panel (from addon-essentials) generates a form from your component's TypeScript types automatically.
You can override controls with argTypes for richer UX:
argTypes: {
variant: {
description: "Visual style of the button",
control: { type: "select" },
options: ["default", "destructive", "outline", "ghost"],
table: {
defaultValue: { summary: "default" },
},
},
}The Actions addon logs onClick calls in real time. Using fn() from @storybook/test (not the old action() helper) means your interaction tests can spy on those calls too.
Step 7: Add Decorators to Mock Next.js APIs
Next.js components often use next/link, next/image, or useRouter. Storybook's @storybook/nextjs adapter handles most of this automatically, but you may need global decorators for providers like theme or i18n.
Edit .storybook/preview.ts:
import type { Preview } from "@storybook/react";
import { initialize, mswLoader } from "msw-storybook-addon";
import "../src/app/globals.css";
initialize();
const preview: Preview = {
loaders: [mswLoader],
parameters: {
nextjs: {
appDirectory: true,
},
},
decorators: [
(Story) => (
<div className="p-4">
<Story />
</div>
),
],
};
export default preview;The nextjs.appDirectory: true parameter tells the adapter you're using the App Router, unlocking support for useRouter, usePathname, and useSearchParams in stories.
To mock a specific router value per story:
export const ActiveLink: Story = {
parameters: {
nextjs: {
router: {
pathname: "/dashboard",
},
},
},
};Step 8: Write Interaction Tests
Interaction tests run inside the Storybook UI and in CI using @storybook/test-runner. They use the same @testing-library/user-event API you already know.
Add a play function to the Button story:
import { expect, userEvent, within } from "@storybook/test";
export const ClickTracking: Story = {
args: {
label: "Submit",
onClick: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole("button", { name: /submit/i });
await userEvent.click(button);
expect(args.onClick).toHaveBeenCalledTimes(1);
},
};Run all interaction tests headlessly:
npx concurrently -k -s first -n "SB,TEST" \
"npm run storybook -- --quiet" \
"npx wait-on tcp:6006 && npx test-storybook"Install the test runner:
npm install --save-dev @storybook/test-runner concurrently wait-onStep 9: Accessibility Testing with the a11y Addon
The @storybook/addon-a11y addon runs axe-core against every story automatically.
Install it:
npm install --save-dev @storybook/addon-a11yAdd it to .storybook/main.ts addons array (already done in Step 3). Open any story and click the Accessibility tab in the addons panel. Violations appear in red, warnings in yellow.
You can configure rules globally or per-story:
export const ContrastCheck: Story = {
parameters: {
a11y: {
config: {
rules: [
{
id: "color-contrast",
enabled: true,
},
],
},
},
},
};To fail CI builds on a11y violations, add this to your test-runner config:
// .storybook/test-runner.ts
import { checkA11y, injectAxe } from "axe-playwright";
import type { TestRunnerConfig } from "@storybook/test-runner";
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page) {
await checkA11y(page, "#storybook-root", {
detailedReport: true,
detailedReportOptions: { html: true },
});
},
};
export default config;Step 10: Auto-Generate Documentation with autodocs
Adding tags: ["autodocs"] to a story's meta generates a full documentation page automatically. The docs page includes:
- Component description (from JSDoc comments on the component)
- Props table with types, descriptions, and default values
- Live interactive examples for every named story
Add JSDoc to your component props for richer docs:
export interface ButtonProps {
/** The text label displayed inside the button */
label: string;
/** Controls the visual style of the button */
variant?: "default" | "destructive" | "outline" | "ghost";
/** Controls the size of the button */
size?: "sm" | "md" | "lg";
/** Shows a spinner and disables the button while true */
loading?: boolean;
}You can also write custom MDX documentation alongside your stories. Create src/components/Button.mdx:
import { Canvas, Meta } from "@storybook/blocks";
import * as ButtonStories from "./Button.stories";
<Meta of={ButtonStories} />
# Button
The `Button` component handles all primary user interactions. Use the `variant` prop
to communicate intent and `size` to fit the surrounding layout.
<Canvas of={ButtonStories.Default} />
## When to use each variant
- **default** — primary actions, confirmation dialogs
- **destructive** — delete, remove, irreversible actions
- **outline** — secondary actions alongside a primary button
- **ghost** — tertiary actions in tight spaces or toolbarsStep 11: Build Storybook for Static Hosting
Generate a static build to host on any CDN:
npm run build-storybookThe output lands in storybook-static/. You can deploy it to Vercel, Netlify, or GitHub Pages. Add it to your CI pipeline so every PR includes an updated component preview.
For GitHub Actions:
name: Storybook CI
on: [push, pull_request]
jobs:
storybook:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run build-storybook
- run: npx concurrently -k -s first -n "SB,TEST"
"npx http-server storybook-static --port 6006 --silent"
"npx wait-on tcp:6006 && npx test-storybook --url http://localhost:6006"Step 12: Visual Regression Testing with Chromatic
Chromatic is the cloud service built by the Storybook team for visual regression testing. It captures pixel snapshots of every story and alerts you to UI diffs in pull requests.
Install the CLI:
npm install --save-dev chromaticRun your first build to establish a baseline:
npx chromatic --project-token=YOUR_TOKENGet your project token from chromatic.com. After the first run, every subsequent CI push will compare against the accepted baseline.
Add to GitHub Actions:
- name: Publish to Chromatic
uses: chromaui/action@latest
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}Troubleshooting
Storybook fails to start after Next.js upgrade
Run npx storybook upgrade to update all Storybook packages to the latest compatible versions.
useRouter throws "invariant expected app router" error
Add nextjs: { appDirectory: true } to .storybook/preview.ts parameters as shown in Step 7.
Tailwind styles not loading in Storybook
Ensure you import globals.css at the top of .storybook/preview.ts. The path must be relative to the .storybook/ directory.
next/image shows broken images
Add staticDirs: ["../public"] to .storybook/main.ts. Images placed in public/ will be served from the Storybook dev server at the same paths.
Interaction tests timing out
Increase the timeout in your test-runner config or check for asynchronous state updates not awaited in the play function.
Next Steps
- Explore @storybook/addon-viewport to test responsive layouts across screen sizes
- Use MSW (Mock Service Worker) to mock API calls in stories with
msw-storybook-addon - Add Storybook Test as a Vitest plugin to run interaction tests inside your existing Vitest suite
- Read the Storybook 8 migration guide if upgrading from v7
Conclusion
Storybook 8 makes component-driven development a first-class part of the Next.js 15 workflow. You now have a setup that:
- Develops components in total isolation, free from page-level concerns
- Documents every component with live interactive examples
- Catches accessibility regressions before they reach users
- Runs interaction tests in CI using the same assertions as your unit tests
- Detects visual regressions with Chromatic snapshots
This workflow scales from a single Button to an enterprise design system with hundreds of components.