Claude Code for Storybook Advanced: Interaction Testing and Addons — Claude Skills 360 Blog
Blog / Frontend / Claude Code for Storybook Advanced: Interaction Testing and Addons
Frontend

Claude Code for Storybook Advanced: Interaction Testing and Addons

Published: April 4, 2027
Read time: 8 min read
By: Claude Skills 360

Storybook 8 advances component development with built-in interaction testing via play functions — const canvas = within(canvasElement); await userEvent.click(canvas.getByRole("button")) simulates user interactions that run in the story. args define story inputs; argTypes configure controls with options, descriptions, and defaults. decorators wrap stories in context providers like React Query, i18n, and theme. composeStories from @storybook/react imports stories into Vitest tests. @storybook/test provides expect, within, userEvent, and waitFor directly. The a11y addon runs axe audits on every story. autodocs: "tag" generates API docs from TypeScript types and JSDoc comments. parameters.backgrounds defines theme variants. Claude Code generates Storybook stories with play functions, decorator patterns, interaction tests, a11y configurations, and the composeStories integration for Vitest.

CLAUDE.md for Storybook Advanced

## Storybook Stack
- Version: storybook >= 8.1, @storybook/react >= 8.1, @storybook/test >= 8.1
- Story: Meta<typeof Component> + StoryObj<typeof Component> — CSF3 format
- Play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); ... }
- userEvent: await userEvent.click/type/selectOptions — from @storybook/test
- Args: args: { label: "Click me" } — argTypes: { label: { control: "text" } }
- Decorator: decorators: [(Story) => <QueryProvider><Story /></QueryProvider>]
- Test: composeStories(import("./Button.stories")) — in Vitest
- a11y: parameters: { a11y: { config: { rules: [{ id: "color-contrast", enabled: true }] } } }
- Docs: tags: ["autodocs"] — generates docs page from types + JSDoc

Component Stories with Play Functions

// components/ui/Button.stories.tsx — interaction testing
import type { Meta, StoryObj } from "@storybook/react"
import { within, userEvent, expect, waitFor } from "@storybook/test"
import { Button } from "./Button"

const meta: Meta<typeof Button> = {
  title: "UI/Button",
  component: Button,
  tags: ["autodocs"],
  parameters: {
    layout: "centered",
    // a11y addon configuration
    a11y: {
      config: {
        rules: [
          { id: "color-contrast", enabled: true },
          { id: "button-name", enabled: true },
        ],
      },
    },
  },
  argTypes: {
    variant: {
      control: "select",
      options: ["primary", "secondary", "ghost", "danger"],
      description: "Visual style variant",
    },
    size: {
      control: "select",
      options: ["sm", "md", "lg"],
    },
    isLoading: {
      control: "boolean",
    },
    isDisabled: {
      control: "boolean",
    },
    onClick: { action: "clicked" },
  },
}

export default meta
type Story = StoryObj<typeof Button>

// Basic story
export const Default: Story = {
  args: {
    children: "Click me",
    variant: "primary",
    size: "md",
  },
}

// Story with click interaction test
export const ClickInteraction: Story = {
  args: {
    children: "Submit",
    variant: "primary",
  },
  play: async ({ canvasElement, args }) => {
    const canvas = within(canvasElement)
    const button = canvas.getByRole("button", { name: "Submit" })

    // Verify initial state
    expect(button).toBeEnabled()
    expect(button).not.toHaveAttribute("aria-busy", "true")

    // Click the button
    await userEvent.click(button)

    // Verify onClick was called (from args.onClick action)
    expect(args.onClick).toHaveBeenCalledOnce()
  },
}

// Loading state test
export const LoadingState: Story = {
  args: {
    children: "Processing",
    isLoading: true,
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)
    const button = canvas.getByRole("button")

    expect(button).toBeDisabled()  // Loading buttons should be disabled
    expect(button).toHaveAttribute("aria-busy", "true")  // Or check for spinner
    expect(canvas.getByText("Processing")).toBeInTheDocument()
  },
}

// All variants
export const AllVariants: Story = {
  render: () => (
    <div className="flex gap-3 flex-wrap">
      {(["primary", "secondary", "ghost", "danger"] as const).map(variant => (
        <Button key={variant} variant={variant}>{variant}</Button>
      ))}
    </div>
  ),
}

Form Story with Validation

// components/forms/LoginForm.stories.tsx — complex form interaction testing
import type { Meta, StoryObj } from "@storybook/react"
import { within, userEvent, expect, waitFor, fn } from "@storybook/test"
import { LoginForm } from "./LoginForm"

const meta: Meta<typeof LoginForm> = {
  title: "Forms/LoginForm",
  component: LoginForm,
  parameters: { layout: "centered" },
  args: {
    onSubmit: fn(),  // fn() — tracked mock function from @storybook/test
  },
}

export default meta
type Story = StoryObj<typeof LoginForm>

export const EmptySubmit: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)

    // Submit empty form
    const submitButton = canvas.getByRole("button", { name: /sign in/i })
    await userEvent.click(submitButton)

    // Verify validation errors appear
    await waitFor(() => {
      expect(canvas.getByText(/email is required/i)).toBeInTheDocument()
      expect(canvas.getByText(/password is required/i)).toBeInTheDocument()
    })
  },
}

export const InvalidEmail: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)

    await userEvent.type(canvas.getByLabelText(/email/i), "notanemail")
    await userEvent.type(canvas.getByLabelText(/password/i), "password123")
    await userEvent.click(canvas.getByRole("button", { name: /sign in/i }))

    await waitFor(() => {
      expect(canvas.getByText(/invalid email/i)).toBeInTheDocument()
    })
  },
}

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

    await userEvent.type(canvas.getByLabelText(/email/i), "[email protected]")
    await userEvent.type(canvas.getByLabelText(/password/i), "SecurePass123!")
    await userEvent.click(canvas.getByRole("button", { name: /sign in/i }))

    await waitFor(() => {
      expect(args.onSubmit).toHaveBeenCalledWith({
        email: "[email protected]",
        password: "SecurePass123!",
      })
    })
  },
}

export const KeyboardNavigation: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)

    // Tab through form
    await userEvent.tab()
    expect(canvas.getByLabelText(/email/i)).toHaveFocus()

    await userEvent.tab()
    expect(canvas.getByLabelText(/password/i)).toHaveFocus()

    await userEvent.tab()
    expect(canvas.getByRole("button", { name: /sign in/i })).toHaveFocus()

    // Submit with Enter
    await userEvent.keyboard("{Enter}")
  },
}

Decorators for Context

// .storybook/decorators.tsx — reusable story decorators
import type { Decorator } from "@storybook/react"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { MemoryRouter, Route, Routes } from "react-router-dom"
import { ThemeProvider } from "@/components/ThemeProvider"

// Wrap with React Query
export const withQueryClient: Decorator = (Story) => {
  const queryClient = new QueryClient({
    defaultOptions: { queries: { retry: false, staleTime: Infinity } },
  })
  return (
    <QueryClientProvider client={queryClient}>
      <Story />
    </QueryClientProvider>
  )
}

// Wrap with Router (for stories using Link/useNavigate)
export const withRouter: Decorator = (Story, context) => {
  const path = context.parameters.router?.path ?? "/"
  return (
    <MemoryRouter initialEntries={[path]}>
      <Routes>
        <Route path="*" element={<Story />} />
      </Routes>
    </MemoryRouter>
  )
}

// Wrap with theme
export const withTheme: Decorator = (Story, context) => {
  const theme = context.globals.theme ?? "light"
  return (
    <ThemeProvider forcedTheme={theme}>
      <div className={theme === "dark" ? "dark" : ""}>
        <Story />
      </div>
    </ThemeProvider>
  )
}
// .storybook/preview.ts — global decorators and parameters
import type { Preview } from "@storybook/react"
import { withQueryClient, withRouter, withTheme } from "./decorators"
import "@/styles/globals.css"

const preview: Preview = {
  decorators: [withTheme, withQueryClient, withRouter],
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/i,
      },
    },
    backgrounds: {
      default: "light",
      values: [
        { name: "light", value: "#ffffff" },
        { name: "dark", value: "#0f172a" },
      ],
    },
    viewport: {
      viewports: {
        mobile: { name: "Mobile", styles: { width: "375px", height: "812px" } },
        tablet: { name: "Tablet", styles: { width: "768px", height: "1024px" } },
        desktop: { name: "Desktop", styles: { width: "1440px", height: "900px" } },
      },
    },
  },
}

export default preview

composeStories for Vitest

// components/ui/Button.test.tsx — run stories as unit tests
import { composeStories } from "@storybook/react"
import { render } from "@testing-library/react"
import { describe, it, expect } from "vitest"
import * as stories from "./Button.stories"

const { Default, LoadingState, ClickInteraction } = composeStories(stories)

describe("Button", () => {
  it("renders primary button", () => {
    const { getByRole } = render(<Default />)
    expect(getByRole("button")).toBeInTheDocument()
  })

  it("shows loading state", () => {
    const { getByRole } = render(<LoadingState />)
    expect(getByRole("button")).toBeDisabled()
  })

  it("runs play function interaction test", async () => {
    const { container } = render(<ClickInteraction />)
    // Execute the play function from the story
    await ClickInteraction.play!({ canvasElement: container })
  })
})

For the Ladle alternative when a faster, simpler story runner with Vite-native builds and no Webpack dependency is preferred for a small component library — Ladle supports MDX and CSF stories with a minimal addon ecosystem but lacks Storybook’s interaction testing ecosystem, see the component story guide. For the Histoire alternative when Vue or Svelte component stories are the primary use case — Histoire is the Vue/Svelte equivalent of Storybook with a similar variant/story API, better suited for those frameworks than Storybook’s React-centric approach, see the component documentation guide. The Claude Skills 360 bundle includes Storybook Advanced skill sets covering play functions, decorators, and composeStories integration. Start with the free tier to try component story generation.

Keep Reading

Frontend

Claude Code for Chart.js Advanced: Custom Plugins and Mixed Charts

Advanced Chart.js patterns with Claude Code — chart.register() for tree-shaking, mixed chart types combining bar and line, custom plugin API with beforeDraw and afterDatasetsDraw hooks, ScriptableContext for computed colors, ChartDataLabels plugin for value labels, chartjs-plugin-zoom for pan and zoom, custom gradient fills via ctx.createLinearGradient, ChartJS annotation plugin for threshold lines, streaming data with chartjs-plugin-streaming, and react-chartjs-2 with useRef and chart instance.

6 min read Jun 27, 2027
Frontend

Claude Code for Nivo: Rich SVG and Canvas Charts

Build rich data visualizations with Nivo and Claude Code — ResponsiveLine and ResponsiveBar for adaptive charts, ResponsiveHeatMap for matrix data, ResponsiveTreeMap for hierarchal data, ResponsiveSunburst for nested proportions, ResponsiveChord for relationship diagrams, ResponsiveCalendar for activity heat maps, ResponsiveNetwork for force graphs, NivoTheme for consistent styling, tooltip customization with sliceTooltip, and motion config for spring animations.

6 min read Jun 26, 2027
Frontend

Claude Code for Victory Charts: React Native and Web Charts

Build cross-platform charts with Victory and Claude Code — VictoryChart, VictoryLine, VictoryBar, and VictoryScatter for web and React Native, VictoryPie for donut charts, VictoryArea for stacked areas, VictoryAxis for custom axes, VictoryTooltip and VictoryVoronoiContainer for hover tooltips, VictoryBrushContainer for range selection, VictoryZoomContainer for pan and zoom, VictoryLegend for series labels, custom theme with VictoryTheme, and VictoryStack for grouped bars.

6 min read Jun 25, 2027

Put these ideas into practice

Claude Skills 360 gives you production-ready skills for everything in this article — and 2,350+ more. Start free or go all-in.

Back to Blog

Get 360 skills free