Skills web-tooling-storybook

Storybook patterns - CSF 3.0, args, controls, autodocs, play functions, interaction testing, visual testing, addons configuration

install
source · Clone the upstream repo
git clone https://github.com/agents-inc/skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/agents-inc/skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/dist/plugins/web-tooling-storybook/skills/web-tooling-storybook" ~/.claude/skills/agents-inc-skills-web-tooling-storybook && rm -rf "$T"
manifest: dist/plugins/web-tooling-storybook/skills/web-tooling-storybook/SKILL.md
source content

Storybook Patterns

Quick Guide: Storybook is a component workshop for building and documenting UI in isolation. Use CSF 3.0 format with typed

meta
and story objects. Define component variants via
args
. Use play functions for interaction testing. Enable
autodocs
for automatic documentation. Configure addons in
.storybook/main.ts
.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,

import type
, named constants)

(You MUST use CSF 3.0 format with

satisfies Meta<typeof Component>
for type-safe story metadata)

(You MUST define a default

meta
export with
component
property - stories without
component
won't render)

(You MUST use

args
for defining props - NOT rendering components with hardcoded props)

(You MUST use play functions with

@storybook/test
utilities for interaction testing - NOT custom event firing)

(You MUST use named exports for stories - default export is reserved for meta object)

</critical_requirements>


Auto-detection: Storybook, .stories.tsx, .stories.ts, CSF, Component Story Format, meta, Meta, StoryObj, args, argTypes, play function, autodocs, @storybook/test, @storybook/addon-essentials

When to use:

  • Developing UI components in isolation without full application context
  • Creating interactive documentation for component libraries
  • Testing component interactions and visual states
  • Building design systems with comprehensive examples
  • Generating automatic API documentation from TypeScript props
  • Visual regression testing

When NOT to use:

  • E2E user flow testing spanning multiple pages (use your E2E tool)
  • Unit testing pure functions without UI (use your test runner directly)
  • Integration testing with real backend services
  • Performance testing and load testing

Key patterns covered:

  • CSF 3.0 story format with TypeScript
  • Args and ArgTypes for props control
  • Play functions for interaction testing
  • Autodocs for automatic documentation
  • MDX for custom documentation pages
  • Decorators and parameters
  • Addon configuration and customization
  • Visual testing integration

Detailed Resources:


<philosophy>

Philosophy

Storybook enables component-driven development - building UI from the bottom up, starting with basic components and progressively combining them into complex interfaces.

Core Principles:

  1. Isolation - Develop components without needing the full application, database, or authentication
  2. Documentation - Stories serve as living documentation that stays in sync with code
  3. Testing - Use stories as test fixtures for interaction, visual, and accessibility testing
  4. Collaboration - Designers, developers, and stakeholders can review components independently

The Story Paradigm:

A "story" captures a specific state of a component. Each story represents one meaningful variation - a button's primary state, disabled state, loading state, etc. Stories are not test cases; they are documented examples.

When to use Storybook:

  • Building component libraries or design systems
  • Developing UI in parallel with backend work
  • Creating documentation for component APIs
  • Visual testing and regression detection
  • Reviewing UI changes in pull requests

When NOT to use:

  • For user journey testing (use E2E tools)
  • For testing business logic without UI
  • For components that require full application context
</philosophy>
<patterns>

Core Patterns

Pattern 1: CSF 3.0 Story Format

Component Story Format 3.0 is the standard for writing stories. It uses typed objects for maximum type safety.

Basic Story Structure

// button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { Button } from "./button";

// Meta object describes the component
const meta = {
  title: "Components/Button",
  component: Button,
  tags: ["autodocs"],
  parameters: {
    layout: "centered",
  },
  argTypes: {
    onClick: { action: "clicked" },
  },
} satisfies Meta<typeof Button>;

export default meta;
type Story = StoryObj<typeof meta>;

// Each named export is a story
export const Primary: Story = {
  args: {
    variant: "primary",
    children: "Primary Button",
  },
};

export const Secondary: Story = {
  args: {
    variant: "secondary",
    children: "Secondary Button",
  },
};

export const Disabled: Story = {
  args: {
    variant: "primary",
    children: "Disabled Button",
    disabled: true,
  },
};

Why good:

satisfies Meta<typeof Button>
provides full type checking while allowing inference, stories are simple objects,
tags: ["autodocs"]
generates documentation automatically


Pattern 2: Args and ArgTypes Configuration

Args define prop values for stories. ArgTypes configure controls in the Storybook UI.

ArgTypes Configuration

const meta = {
  title: "Components/Card",
  component: Card,
  argTypes: {
    // Control type for enums
    variant: {
      control: { type: "select" },
      options: ["default", "outline", "filled"],
      description: "Visual variant of the card",
    },
    // Control type for booleans
    isLoading: {
      control: { type: "boolean" },
      description: "Shows loading skeleton",
    },
    // Control type for strings
    title: {
      control: { type: "text" },
      description: "Card title text",
    },
    // Range control for numbers
    padding: {
      control: { type: "range", min: 0, max: 64, step: 4 },
      description: "Padding in pixels",
    },
    // Color picker
    backgroundColor: {
      control: { type: "color" },
    },
    // Hide from controls
    internalId: {
      table: { disable: true },
    },
    // Action handler
    onClose: {
      action: "closed",
    },
  },
} satisfies Meta<typeof Card>;

Default Args

const meta = {
  title: "Components/Input",
  component: Input,
  // Default args applied to all stories
  args: {
    placeholder: "Enter text...",
    disabled: false,
  },
} satisfies Meta<typeof Input>;

export default meta;
type Story = StoryObj<typeof meta>;

// Story inherits default args, only overrides what changes
export const Default: Story = {};

export const WithLabel: Story = {
  args: {
    label: "Email address",
  },
};

export const Disabled: Story = {
  args: {
    disabled: true,
  },
};

Why good: centralized defaults reduce duplication, argTypes provide interactive documentation, control types match prop types for intuitive editing


Pattern 3: Decorators for Context

Decorators wrap stories with providers, layouts, or styling context.

Component-Level Decorators

const meta = {
  title: "Components/Modal",
  component: Modal,
  decorators: [
    // Add padding around the story
    (Story) => (
      <div style={{ padding: "3rem" }}>
        <Story />
      </div>
    ),
    // Wrap with theme provider
    (Story) => (
      <ThemeProvider theme="light">
        <Story />
      </ThemeProvider>
    ),
  ],
} satisfies Meta<typeof Modal>;

Global Decorators

// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { ThemeProvider } from "../src/theme-provider";

const preview: Preview = {
  decorators: [
    (Story) => (
      <ThemeProvider>
        <Story />
      </ThemeProvider>
    ),
  ],
};

export default preview;

Story-Level Decorators

export const InDarkMode: Story = {
  decorators: [
    (Story) => (
      <div className="dark" style={{ background: "#1a1a1a", padding: "2rem" }}>
        <Story />
      </div>
    ),
  ],
  args: {
    children: "Dark Mode Button",
  },
};

Why good: decorators keep component code clean, global decorators ensure consistent context, story decorators allow specific variations


Pattern 4: Parameters for Story Configuration

Parameters configure Storybook features and addons per story, component, or globally.

Common Parameters

const meta = {
  title: "Components/Dialog",
  component: Dialog,
  parameters: {
    // Layout options: centered, fullscreen, padded
    layout: "centered",

    // Background options
    backgrounds: {
      default: "light",
      values: [
        { name: "light", value: "#ffffff" },
        { name: "dark", value: "#1a1a1a" },
        { name: "brand", value: "#0066ff" },
      ],
    },

    // Viewport configuration
    viewport: {
      defaultViewport: "mobile1",
    },

    // Disable specific controls panel
    controls: {
      expanded: true,
      sort: "requiredFirst",
    },

    // Documentation page order
    docs: {
      description: {
        component: "A modal dialog for user confirmations.",
      },
    },
  },
} satisfies Meta<typeof Dialog>;

Story-Specific Parameters

export const MobileView: Story = {
  parameters: {
    viewport: {
      defaultViewport: "mobile1",
    },
    chromatic: {
      viewports: [320, 768],
    },
  },
};

export const FullPage: Story = {
  parameters: {
    layout: "fullscreen",
  },
};

Why good: parameters configure addon behavior declaratively, can be set at any level (global, component, story), consistent API across all addons


Pattern 5: Play Functions for Interaction Testing

Play functions enable interaction testing within stories using the

@storybook/test
utilities.

Basic Interaction Testing

import type { Meta, StoryObj } from "@storybook/react";
import { expect, fn, userEvent, within } from "@storybook/test";
import { LoginForm } from "./login-form";

const meta = {
  title: "Forms/LoginForm",
  component: LoginForm,
  args: {
    onSubmit: fn(),
  },
} satisfies Meta<typeof LoginForm>;

export default meta;
type Story = StoryObj<typeof meta>;

export const FilledForm: Story = {
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement);

    // Find elements
    const emailInput = canvas.getByLabelText(/email/i);
    const passwordInput = canvas.getByLabelText(/password/i);
    const submitButton = canvas.getByRole("button", { name: /sign in/i });

    // Interact with form
    await userEvent.type(emailInput, "test@example.com");
    await userEvent.type(passwordInput, "password123");
    await userEvent.click(submitButton);

    // Assert callback was called
    await expect(args.onSubmit).toHaveBeenCalledWith({
      email: "test@example.com",
      password: "password123",
    });
  },
};

Composing Play Functions

export const Default: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);
    await expect(canvas.getByRole("button")).toBeInTheDocument();
  },
};

export const Clicked: Story = {
  play: async (context) => {
    // Run the Default story's play function first
    await Default.play?.(context);

    const canvas = within(context.canvasElement);
    const button = canvas.getByRole("button");

    await userEvent.click(button);
    await expect(button).toHaveAttribute("data-state", "pressed");
  },
};

See examples/testing.md for more play function patterns.

Why good: tests run in actual browser environment, reuses Testing Library queries for consistency, play functions double as documentation


Pattern 6: Render Functions for Complex Stories

Use the

render
property when you need more control than
args
provides.

Custom Render Function

export const WithMultipleItems: Story = {
  render: (args) => (
    <List>
      <ListItem {...args} title="First Item" />
      <ListItem {...args} title="Second Item" />
      <ListItem {...args} title="Third Item" />
    </List>
  ),
  args: {
    isSelected: false,
  },
};

Render with Hooks

export const Controlled: Story = {
  render: function Render(args) {
    const [isOpen, setIsOpen] = useState(false);

    return (
      <div>
        <Button onClick={() => setIsOpen(true)}>Open Dialog</Button>
        <Dialog {...args} open={isOpen} onClose={() => setIsOpen(false)} />
      </div>
    );
  },
  args: {
    title: "Controlled Dialog",
  },
};

Why good: render functions allow complex compositions, named functions enable hooks, args still control individual props


Pattern 7: Story Organization

Organize stories with a clear hierarchy that reflects your component structure.

Title Hierarchy

// Organized by category
const meta = {
  title: "Components/Forms/Input", // Creates nested navigation
  component: Input,
} satisfies Meta<typeof Input>;

// Alternative structures:
// "Design System/Atoms/Button"
// "Pages/Dashboard/Header"
// "Features/Authentication/LoginForm"

Naming Conventions

// Good: Descriptive story names
export const Default: Story = {};
export const WithIcon: Story = {};
export const Disabled: Story = {};
export const Loading: Story = {};
export const WithValidationError: Story = {};

// Bad: Unclear names
export const Story1: Story = {};
export const Test: Story = {};

Story Order

const meta = {
  title: "Components/Button",
  component: Button,
  parameters: {
    docs: {
      // Control story order in docs
      page: () => (
        <>
          <Title />
          <Subtitle />
          <Description />
          <Primary />
          <Controls />
          <Stories />
        </>
      ),
    },
  },
} satisfies Meta<typeof Button>;

Why good: hierarchical titles create intuitive navigation, descriptive names serve as documentation, consistent patterns across the codebase

</patterns>
<integration>

Integration Guide

Works with your component framework:

  • Stories import and render your actual components
  • TypeScript types flow from component props to story args
  • Your styling solution works as-is in Storybook (CSS modules, utility classes, etc.)

Works with your testing tools:

  • Play functions use Testing Library queries
  • Stories can be imported as test fixtures in your test runner
  • Visual testing tools use stories as test cases

Works with your CI/CD:

  • build-storybook
    creates static site for deployment
  • Storybook test addon integrates with CI for interaction tests
  • Visual regression tools integrate via CI

Boundary guidance:

  • Storybook handles: component isolation, documentation, controls, interaction testing, visual states
  • Your E2E tool handles: full user journeys, navigation flows
  • Your styling solution handles: actual CSS implementation
</integration>

<red_flags>

RED FLAGS

High Priority Issues:

  • Using default export for stories - Default export is reserved for meta; stories must be named exports
  • Hardcoding props in JSX instead of using args - Defeats the purpose of controls; makes stories non-interactive
  • Missing
    component
    property in meta
    - Stories won't render correctly; autodocs won't work
  • Using
    fireEvent
    instead of
    userEvent
    in play functions
    - Doesn't simulate real user behavior
  • Importing from
    @storybook/testing-library
    - Deprecated; use
    @storybook/test
    instead
  • Using
    argTypesRegex
    for play function handlers
    - Implicit actions cannot be used in play functions; use explicit
    fn()

Medium Priority Issues:

  • Not using
    satisfies Meta<typeof Component>
    - Loses type safety on story args
  • Putting business logic in play functions - Play functions are for interaction, not logic testing
  • Missing
    tags: ["autodocs"]
    - Won't generate automatic documentation
  • Overly complex render functions - If render is complex, component API may need refactoring

Common Mistakes:

  • Forgetting to
    await
    userEvent actions in play functions
  • Using
    any
    type instead of
    StoryObj<typeof meta>
  • Not setting up global decorators for providers (ThemeProvider, etc.)
  • Putting test assertions in stories without play functions

Gotchas & Edge Cases:

  • args
    are serializable - functions must use
    action
    or
    fn()
    from
    @storybook/test
  • Play functions run after render - use
    waitFor
    for async content
  • Decorators run outermost to innermost - order matters for context
  • canvas.getByRole
    uses Testing Library - same accessibility rules apply
  • Hot reload preserves args state - may need manual refresh for some changes
  • Storybook 8 uses
    react-docgen
    by default (not
    react-docgen-typescript
    ) - imported types may not be extracted
  • In Storybook 8.2+, use
    initialGlobals
    instead of deprecated
    globals
    in preview.ts
  • Built-in tags:
    dev
    (sidebar visibility),
    test
    (test inclusion),
    autodocs
    (docs generation) - use
    !tag
    to remove inherited tags
  • Storybook test addon (8.4+) is recommended over legacy test-runner for Vite projects - provides Storybook UI integration
  • Storybook 10 is ESM-only - requires
    "type": "module"
    in package.json and Node 20.16+

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST use CSF 3.0 format with

satisfies Meta<typeof Component>
for type-safe story metadata)

(You MUST define a default

meta
export with
component
property - stories without
component
won't render)

(You MUST use

args
for defining props - NOT rendering components with hardcoded props)

(You MUST use play functions with

@storybook/test
utilities for interaction testing - NOT custom event firing)

(You MUST use named exports for stories - default export is reserved for meta object)

Failure to follow these rules will produce untestable stories, missing documentation, and broken controls.

</critical_reminders>