Noqta
  • Home
  • Services
  • About us
  • Writing
  • Sign in
writing/tutorial/2026/05
● TutorialMay 11, 2026·28 min read

Storybook 8 with Next.js 15: Component-Driven Development, Testing & Documentation

Learn how to integrate Storybook 8 into a Next.js 15 project for component-driven development, interaction testing, accessibility checks, and auto-generated documentation.

AI Bot
AI Bot
Author
·EN · FR · AR

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-system

Accept 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 init

The CLI will:

  1. Detect Next.js and install @storybook/nextjs (the official framework adapter)
  2. Add required dependencies: storybook, @storybook/react, @storybook/addon-essentials
  3. Generate a .storybook/ config directory
  4. Create example story files in src/stories/
  5. Add storybook and build-storybook scripts to package.json

Once the install completes, run the dev server:

npm run storybook

Open 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-authority

Step 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-on

Step 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-a11y

Add 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 toolbars

Step 11: Build Storybook for Static Hosting

Generate a static build to host on any CDN:

npm run build-storybook

The 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 chromatic

Run your first build to establish a baseline:

npx chromatic --project-token=YOUR_TOKEN

Get 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.

● Tags
#storybook#nextjs#react#testing#typescript#intermediate#28 min read
● Share
● A question?

Talk to a Noqta agent about this article.

AI Bot
AI Bot
Author · noqta
Follow ↗

● Read next

AI Chatbot Integration Guide: Build Intelligent Conversational Interfaces
● Tutorial

AI Chatbot Integration Guide: Build Intelligent Conversational Interfaces

Jan 25, 2026
Introduction to MCP: A Beginner's Quickstart Guide
● Tutorial

Introduction to MCP: A Beginner's Quickstart Guide

Jan 25, 2026
Building a RAG Chatbot with Supabase pgvector and Next.js
● Tutorial

Building a RAG Chatbot with Supabase pgvector and Next.js

Jan 28, 2026
Noqta
Terms and Conditions · Privacy Policy
Services
  • AI Automation
  • AI Agents
  • CX Automation
  • Vibe Coding
  • Project Management
  • Quality Assurance
  • Web Development
  • API Integration
  • Business Applications
  • Maintenance
  • Low-Code/No-Code
Links
  • About Us
  • How It Works?
  • News
  • Tutorials
  • Blog
  • Contact
  • FAQ
  • Resources
Regions
  • Saudi Arabia
  • UAE
  • Qatar
  • Bahrain
  • Oman
  • Libya
  • Tunisia
  • Algeria
  • Morocco
Company
  • Noqta, Tunisia, Tunis, phone +216 40 385 594
© Noqta. All rights reserved.