Back to list
Higashi-Kota

storybook

by Higashi-Kota

0🍴 0📅 Jan 3, 2026

SKILL.md


name: storybook description: | Storybook CSF3 story authoring methodology for UI component catalogs. Prioritizes visual showcase patterns over exhaustive argTypes enumeration. Reference for creating scannable, maintainable component documentation.

Storybook Skill

Philosophy: Catalog over Controls

Problem with argTypes exhaustion:

  • Dozens of stories for every prop combination
  • Hard to scan and find what you need
  • Maintenance burden grows exponentially
  • Controls panel becomes primary interaction

Solution: Visual Showcase Pattern

  • One story shows multiple variants in a grid
  • Immediate visual comparison
  • Self-documenting code
  • Controls for interactive exploration only

CSF3 Story Patterns

1. Showcase Story (Primary Pattern)

Display all variants of a prop in a single story:

/**
 * All button variants displayed together for visual comparison.
 */
export const Variants: Story = {
  render: () => (
    <div className="grid grid-cols-2 gap-4 items-center">
      <span className="text-sm text-muted-foreground">Primary</span>
      <Button variant="primary">Primary</Button>

      <span className="text-sm text-muted-foreground">Secondary</span>
      <Button variant="secondary">Secondary</Button>

      <span className="text-sm text-muted-foreground">Ghost</span>
      <Button variant="ghost">Ghost</Button>

      <span className="text-sm text-muted-foreground">Destructive</span>
      <Button variant="destructive">Delete</Button>
    </div>
  ),
}

2. State Matrix Story

Show component states in a grid:

/**
 * Button states: normal, disabled, and loading.
 */
export const States: Story = {
  render: () => (
    <div className="grid grid-cols-3 gap-4 items-center">
      {/* Header row */}
      <span className="text-sm text-muted-foreground">Normal</span>
      <span className="text-sm text-muted-foreground">Disabled</span>
      <span className="text-sm text-muted-foreground">Loading</span>

      {/* Primary row */}
      <Button>Save</Button>
      <Button disabled>Save</Button>
      <Button loading>Saving...</Button>

      {/* Secondary row */}
      <Button variant="secondary">Cancel</Button>
      <Button variant="secondary" disabled>Cancel</Button>
      <Button variant="secondary" loading>Loading...</Button>
    </div>
  ),
}

3. Use Case Story

Show real-world usage patterns:

/**
 * Common button patterns in real UI contexts.
 */
export const UseCases: Story = {
  render: () => (
    <div className="flex flex-col gap-6">
      {/* Form Actions */}
      <div className="flex gap-2 justify-end p-4 border border-border rounded-md">
        <Button variant="ghost">Cancel</Button>
        <Button variant="primary" iconBefore={<Save />}>
          Save Changes
        </Button>
      </div>

      {/* Destructive Action */}
      <div className="flex gap-2 p-4 border border-border rounded-md">
        <Button variant="destructive" iconBefore={<Trash2 />}>
          Delete Item
        </Button>
      </div>
    </div>
  ),
}

4. A11y Test Story (with play function)

Verify accessibility requirements:

/**
 * Accessibility test: Verifies aria-label is properly set.
 */
export const A11yAriaLabel: Story = {
  args: {
    icon: <X />,
    "aria-label": "Close panel",
  },
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement)
    const button = canvas.getByRole("button", { name: "Close panel" })
    await expect(button).toBeInTheDocument()
    await expect(button).toHaveAccessibleName("Close panel")
  },
}

Story Organization

Naming Convention

Components/
├── Button           # Component name
│   ├── Variants     # Visual showcase
│   ├── Sizes        # Size showcase
│   ├── States       # State matrix
│   ├── WithIcons    # Feature showcase
│   ├── UseCases     # Real-world examples
│   ├── A11yBasic    # A11y verification
│   └── A11yFocus    # A11y verification

Story Order Priority

  1. Showcase stories first - Visual comparison grids
  2. Feature stories - Icons, loading, etc.
  3. Use case stories - Real-world patterns
  4. A11y test stories - Verification with play functions

Meta Configuration

Minimal argTypes

const meta: Meta<typeof IconButton> = {
  title: "Components/IconButton",
  component: IconButton,
  parameters: {
    layout: "centered",
    docs: {
      description: {
        component: `Brief component description with key features.`,
      },
    },
  },
  argTypes: {
    // Only disable complex/function props
    icon: { control: false },
    onClick: { control: false },
    // Let simple props auto-generate controls
    variant: {
      control: "select",
      options: ["default", "ghost", "destructive"],
    },
  },
}

Layout Parameters

LayoutUse Case
centeredSingle components, buttons, inputs
paddedContainers, cards, panels
fullscreenPage layouts, full-width components

Play Function Patterns

Role-Based Queries (Preferred)

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

  // ✅ Query by role
  const button = canvas.getByRole("button", { name: "Submit" })
  const input = canvas.getByRole("textbox", { name: "Email" })
  const menu = canvas.getByRole("menu")

  // ✅ Verify accessibility
  await expect(button).toHaveAccessibleName("Submit")
  await expect(button).not.toBeDisabled()
}

User Interaction

play: async ({ canvasElement }) => {
  const canvas = within(canvasElement)
  const user = userEvent.setup()

  const button = canvas.getByRole("button", { name: "Open menu" })
  await user.click(button)

  // Verify state change
  await expect(button).toHaveAttribute("aria-expanded", "true")

  const menu = canvas.getByRole("menu")
  await expect(menu).toBeInTheDocument()
}

Focus Verification

play: async ({ canvasElement }) => {
  const canvas = within(canvasElement)
  const button = canvas.getByRole("button")

  button.focus()
  await expect(button).toHaveFocus()
}

Step Function for Complex Flows

Organize multi-step interactions for better debugging:

import { step } from "storybook/test"

play: async ({ canvasElement }) => {
  const canvas = within(canvasElement)
  const user = userEvent.setup()

  await step("Open the dialog", async () => {
    await user.click(canvas.getByRole("button", { name: "Settings" }))
    await expect(canvas.getByRole("dialog")).toBeInTheDocument()
  })

  await step("Fill in the form", async () => {
    await user.type(canvas.getByLabelText("Name"), "Test User")
    await user.selectOptions(canvas.getByLabelText("Theme"), "dark")
  })

  await step("Submit and verify", async () => {
    await user.click(canvas.getByRole("button", { name: "Save" }))
    await expect(canvas.getByText("Settings saved")).toBeInTheDocument()
  })
}

A11y Testing Checklist

Each component should have stories verifying:

  • aria-label / aria-labelledby for unlabeled elements
  • aria-expanded for expandable elements
  • aria-pressed for toggle buttons
  • aria-selected for selectable items
  • Focus visibility and keyboard navigation
  • Role attributes (button, menu, tab, tablist, etc.)

Form Component Story Patterns

BorderRadiusCheck Story

Add a story using descender-heavy text ("gggjjjyyyqqqppp") to verify text doesn't clip at rounded corners. Test all size variants.

Sizes, States, WithLabel, BorderRadiusCheck, A11yWithLabel, A11yDisabled, A11yError

File Structure

packages/ui/
├── .storybook/
│   ├── main.ts          # Storybook config
│   ├── preview.ts       # Global decorators, a11y config
│   └── storybook.css    # Theme import + @source
├── src/
│   └── components/
│       ├── Button.tsx
│       ├── Button.stories.tsx
│       ├── IconButton.tsx
│       └── IconButton.stories.tsx

Configuration

main.ts

import type { StorybookConfig } from "@storybook/react-vite"
import tailwindcss from "@tailwindcss/vite"

const config: StorybookConfig = {
  stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
  addons: [
    "@storybook/addon-vitest",  // Component testing
    "@storybook/addon-a11y",    // Accessibility panel
    "@storybook/addon-docs",    // Documentation
  ],
  framework: "@storybook/react-vite",
  async viteFinal(config) {
    config.plugins = config.plugins
      ? [...config.plugins, tailwindcss()]
      : [tailwindcss()]
    return config
  },
}

preview.ts

import type { Preview } from "@storybook/react-vite"
import "./storybook.css"

const preview: Preview = {
  parameters: {
    a11y: {
      config: {
        rules: [
          { id: "color-contrast", enabled: true },
          { id: "aria-required-attr", enabled: true },
          { id: "button-name", enabled: true },
          { id: "label", enabled: true },
        ],
      },
    },
    docs: {
      toc: true,  // Table of contents in docs
    },
  },
}

storybook.css

@import "@internal/theme/index.css";

@source "../src/**/*.tsx";

Anti-Patterns

❌ One story per variant

// DON'T: Creates too many sidebar entries
export const Primary: Story = { args: { variant: "primary" } }
export const Secondary: Story = { args: { variant: "secondary" } }
export const Ghost: Story = { args: { variant: "ghost" } }
export const Destructive: Story = { args: { variant: "destructive" } }

✅ Single showcase story

// DO: One story showing all variants
export const Variants: Story = {
  render: () => (
    <div className="grid grid-cols-2 gap-4">
      <Button variant="primary">Primary</Button>
      <Button variant="secondary">Secondary</Button>
      <Button variant="ghost">Ghost</Button>
      <Button variant="destructive">Destructive</Button>
    </div>
  ),
}

❌ Relying on Controls for documentation

// DON'T: Users must interact with controls to see variants
export const Default: Story = {
  args: {
    variant: "primary",
    size: "md",
  },
}

✅ Visual documentation

// DO: All variants visible at once
export const Sizes: Story = {
  render: () => (
    <div className="flex items-center gap-4">
      <Button size="sm">Small</Button>
      <Button size="md">Medium</Button>
      <Button size="lg">Large</Button>
    </div>
  ),
}

Portable Stories (Storybook 8+)

Use stories in external test runners:

// Button.test.tsx
import { composeStories } from "@storybook/react"
import { render, screen } from "@testing-library/react"
import * as stories from "./Button.stories"

const { Primary, Disabled } = composeStories(stories)

test("renders primary button", () => {
  render(<Primary />)
  expect(screen.getByRole("button")).toBeInTheDocument()
})

test("disabled button is not interactive", () => {
  render(<Disabled />)
  expect(screen.getByRole("button")).toBeDisabled()
})

A11y Testing with vitest-axe

// accessibility.test.tsx
import { composeStories } from "@storybook/react"
import { render } from "@testing-library/react"
import { axe } from "vitest-axe"
import * as ButtonStories from "./Button.stories"
import * as TreeViewStories from "./TreeView.stories"

const { Default: ButtonDefault } = composeStories(ButtonStories)
const { Default: TreeViewDefault } = composeStories(TreeViewStories)

describe("Accessibility", () => {
  test("Button axe-core check", async () => {
    const { container } = render(<ButtonDefault />)
    expect(await axe(container)).toHaveNoViolations()
  })

  test("TreeView axe-core check", async () => {
    const { container } = render(<TreeViewDefault />)
    expect(await axe(container)).toHaveNoViolations()
  })
})

Benefits:

  • Reuse stories in Vitest/Jest
  • Single source of truth for component states
  • Decorators and args automatically applied
  • Faster a11y validation than E2E (milliseconds)

References

Score

Total Score

60/100

Based on repository quality metrics

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新がある

0/10
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

+5
タグ

1つ以上のタグが設定されている

0/5

Reviews

💬

Reviews coming soon