About sb-stories
sb-stories is a skill for developers authoring Storybook stories for components and pages. It prioritizes real usage and meaningful states, then uses validation scripts to check the work.
Key features
- 01Author stories for meaningful component states
- 02Prioritize variants using usage data
- 03Import real pages and mock data
- 04Handle Controls, overlays, and interactions
- 05Validate stories with scripts
Use cases
- 01Add Storybook state stories for existing components
- 02Build page stories with real pages and varied data states
- 03Author and validate component stories in batches
MCP support
The skill selects authoring guidance according to whether the project has MCP configured; the WITH_MCP path uses MCP to supply CSF3 conventions.
Install
Copy the command and run it in a terminal.
npx skills add https:/ / github.com/ strongeron/ storybook-workbench --skill sb-storiesAfter installing, type/sb-storiesin Claude Code to confirm it loaded.
Bundled files
References and scripts installed along with SKILL.md.
CONTEXT.mdagents/openai.yamlreferences/anti-patterns.mdcomposition-patterns.mddirectory-structure.mdextraction-workflow.mdfactory-patterns.mdstorybook-surface.mdtest-wiring.mdvalidate-workflow.mdwith-mcp.mdwithout-mcp.mdscripts/check-story-ready.shdiscover-runtime.pyextract-prop-shapes.shextract-states.shpage-patterns.pyscaffold-factory.shscaffold-page-story.pyvalidate-stories.shtemplates/controlled-component-story.tsxDetails
From the project READMEsb-stories — one component, its real states
The default Build mode. The component exists in src/components/; you write its visible-states story.
Before authoring (ask yourself)
- Is it actually used? Check
components.real[]in.storybook/project-inventory.json. If it's indead[], remove it — don't write a story. If it'svendor(shadcnui/), deprioritize. - Which states change behavior/appearance materially? Refuse Cartesian combinations. Per-primitive
minimum tables in
references/anti-patterns.md(Button 8, Input 8, Modal 5, Form 6). Read.storybook/component-states.jsoninstead of guessing — if it's missing, generate it first:scripts/extract-states.sh. (Bigger extraction context:references/extraction-workflow.md.) To render all those states on one canvas, use theStateGridwrapper (variants × states →StateMatrix) fromsb-wrappers— don't hand-roll the grid. These are the component-time wrappers; the data wrappers (ProjectInventory / DesignSystemHealth / AppFlowGraph …) come after their own steps. - Which states does THIS app actually ship? Read
.storybook/component-usage.json(sb-inventory'sextract-component-usage.sh— generate it if missing). Useprops.<prop>.valuesto prioritize the variants real call sites pass; for anything indeclaredButUnused(e.g.variant=dangernever used), still author the state for completeness but tag it['usage:unused']and note "not used in this app" — don't pad the catalog with states prod never renders. - You don't author per-component docs by hand — they're composed once. Every component's autodocs
page already gains a "Real usage in this app" band: the
UsageSectionblock (wired once intopreview.tsdocs.pagebysb-setup) renders a "Where it's used" map per component — the pages it lands on, what nests it, what it renders, the tokens it pulls — read from the usage graph (component-pages.json). So when you add a story, don't hand-add a usage block; just runrefresh-usage.shso the graph is current and the band populates. To explore the whole graph interactively (any token / component / page → everywhere it's used, clickable), scaffold theUsageExplorerwrapper (sb-wrappers). Details + the docs.page composition live insb-inventory(§ "Real usage in autodocs") andsb-setup(docs-page composition). - Factory? YES if 3+ stories share a data shape. Read
.storybook/prop-shapes.json(candidates flagged withliveUsages) — if missing, runscripts/extract-prop-shapes.shfirst. Thenscripts/scaffold-factory.sh <Type> <import-path>and fill the deterministic stub (references/factory-patterns.md); otherwise inlineargs. - Title taxonomy? Match
storySort.order; if the project has none, pick one viareferences/directory-structure.md. - Does a
playactually earn its place? Only write one for an interaction, async data, a portal, a CSS-driven state, or accessibility — never a baretoBeVisible()(anti-pattern 34). And the project needs exactly oneCssCheck(anti-pattern 33): one story asserting a realgetComputedStyletoken value, the only proof the shared preview loaded the app's CSS. Both come fromnpx storybook ai setup's prompt;validate-stories.shcheck 13 + the project tally enforce them. - A "playground" / showcase story is Controls-driven, not click/hover. If a reviewer asks "what is
this playground story — can I click or hover it?": a playground is the default story with its Controls
panel — you change
args(variant, size, state) in the Controls tab and the canvas re-renders. It is NOT canvas interaction; hover/focus/active live in aStateGridinteraction matrix (addon-pseudo-states), and scripted click→assert flows live in aplayfunction. So: Controls = try props · pseudo-states = hover/focus columns ·play= a real interaction. Say which one the story is. - Is the Controls panel wired? A component story must expose a usable Controls panel — it's the
reviewer's prop sandbox and powers the autodocs ArgTypes table. The react-vite default
(
react-docgen) does NOT infer TS unions into selects, so declareargTypesfor every enum/union prop (control: 'select'|'inline-radio'+options), group withtable.category, hide escape-hatch / non-serializable props (className, refs, icon / callback / data props), and disable controls on render-only showcase stories (parameters: { controls: { disable: true } }). Full pattern + the docgen gotcha:references/without-mcp.md§13. (The panel itself must be visible —sb-setupwrites amanager.tswithshowPanel: true;sb-audit'saudit-controls.shflags any component story missing this wiring.)
Authoring source (mutually exclusive — load exactly one)
grep -q '@storybook/addon-mcp' package.json && test -f .mcp.json && echo WITH_MCP || echo WITHOUT_MCP
WITH_MCP→references/with-mcp.md(MCP injects CSF3 conventions; you focus on judgment). Tool names change between Storybook minors; the current names are inreferences/storybook-surface.md.WITHOUT_MCP→references/without-mcp.md(13 verification gaps + 4 critical SB10 patterns).- Controlled components (Switch/Toggle/Checkbox/Tabs/Accordion/Select) start from
templates/controlled-component-story.tsx— theuseArgssync is what AI gets wrong.
Where the file goes (ASK first — don't scatter the repo). Read storiesLocation from
.storybook/audit/status.md (the single rule lives in CONTEXT.md §STORIES LOCATION).
- If it's unset (e.g. the repo already had Storybook so
sb-setupnever asked), STOP and ASK the user before writing any story — never guess, never co-locate silently. UseAskUserQuestion(Claude) /request_user_input(Codex), or a numbered list where no blocking tool exists:Where should I save the stories? (everything else already lives under
.storybook/.).storybook/stories/(recommended — one place, isolated;src/untouched, one removable folder)- Co-located
src/**/<Name>.stories.tsx(for a project you own long-term) - A custom folder (you name it — still kept to that one place)
Then record it in
.storybook/audit/status.mdasstoriesLocation: <isolated|colocated|PATH>, make suremain.tsstoriesincludes that path, and proceed. Recommend option 1.
isolated(or.storybook/stories/) → write under.storybook/stories/mirroring the tree (.storybook/stories/components/CourseCard.stories.tsx), importing the component via the@/alias. The dev server does not watch.storybook/stories/: a story file added, moved or deleted there stays out of (or stale in) the index until Storybook restarts, and every MCP tool reports "No story found" for it. Restart after changing files there (verified on 10.6.0;src/stories update live).colocated→src/components/<X>/<X>.stories.tsx. A custom path → write there, every story.
Whatever the answer, all stories go to that one location — never a mix.
Title (the in-Storybook path, separate from the file path): match .storybook/preview.ts
storySort.order; else Components/<Domain>/<Name>. An Overview / Spec / hub story is an entry
point — pin it to the top of its root via storySort.order (a root-level hub first; a per-feature
Overview/Spec first in its group's sub-order with a '*' tail), never let it sort alphabetically into
the middle of the content it summarizes. See references/directory-structure.md → "Overview / spec / hub".
Pages — real-page capture (Mode A)
For a page (a route/view under pages/ · app/ · routes/ · views/) do NOT recreate it.
Run scripts/page-patterns.py <root> first — per page it reports importable, component,
layout, dataHook + dataType (the mock signal), sections (render order, ui/ excluded),
gridHint; plus sharedSections[]. Then pick the mode off importable:
importable: true(page has a default-export component) → import the real page as-is and mock ONLY its data layer. Scaffold withscripts/scaffold-page-story.py <root> <page-suffix>: it emits aPages/<Name>story that imports the real page + wires the detected provider (InertiausePage/ router / store), with props seeded from a factory keyed ondataType(scripts/scaffold-factory.sh <dataType> <import-path>, perreferences/factory-patterns.md). Add one story per materially-different data state (empty / populated / error) — different factory inputs, never different markup. Never re-author the page's JSX (anti-pattern 27): the layout, columns, and components are the real page's, not yours.importable: false(assembled inline / no single component) → fall back to Page Composition (references/composition-patterns.mdPattern 4): assemble from the realsections— still factory-backed, still real components.
sharedSections[] (a section rendered by ≥2 pages) are the reusable page-pattern pieces — give each
its own Components/* story so pages compose them, not duplicate them.
Overlays (Dialog / Modal / Sheet / Drawer / Popover) in autodocs
An open overlay portals a position:fixed inset-0 overlay to document.body. Rendered inline on
the autodocs page (as the Primary block does), that overlay escapes over the whole Docs page —
Title, the "Real usage" section, Controls all vanish behind a blank backdrop. So for any overlay
component with autodocs, scope the story to its own iframe:
parameters: {
layout: 'fullscreen',
docs: { story: { inline: false, height: '640px' } }, // portal stays inside the frame; Docs prose stays readable
}
The story view (one story, full canvas) is unaffected — this is only for the Docs page. Don't reach for it on non-overlay components (inline rendering is lighter).
The page's own dataHook is just its data; the preview must also supply the provider TREE + root
CSS the page renders under. Read those from .storybook/runtime.json (scripts/discover-runtime.py . --out .storybook/runtime.json if it's missing) —
providers[], rootCss, portals[], network.needsMsw — they're set up once in the shared preview
by sb-setup, so a page story rarely re-wires them. Never re-derive by shell scan what a script
already wrote to .storybook/*.json — cite the field.
Batch (several components)
Write one story per component (each covering only its real states), then gate each with
scripts/validate-stories.sh. On Claude Code you can speed a batch up by writing components in
parallel with the Agent tool, but it's the same work — no special sub-agent needed.
Gate before done
SKILL=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}
CORE=${CLAUDE_PLUGIN_ROOT:-${CLAUDE_PLUGIN_ROOT}}
"$CORE/scripts/validate-stories.sh" path/to/Foo.stories.tsx
# --strict adds tsc + eslint · --diff lints only changed stories
# For the "give me confidence" verdict (setup readiness preflight + conformance in one),
# run "$SKILL/scripts/check-story-ready.sh" path/to/Foo.stories.tsx instead — CONFIDENT when discovery JSONs are present.
Exits non-zero on any FAIL — fix before continuing. If you wrote a play, also dispatch the
judgment sub-agent (see references/validate-workflow.md); bash can't verify a play
is meaningful. To make stories an agent-runnable CLI gate (headless vitest + a11y), see
references/test-wiring.md.