Use when a component library needs a workshop — one story per concept, JSDoc summaries the manifest can carry, stories run as tests, and an opt-in MCP channel that replaces guessing with querying.
Installs into .claude/skills of the current project.
Are you the author of Storybook Workshop?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/event4u-app-storybook-workshop)
---
model_tier: medium
name: storybook-workshop
description: "Use when a component library needs a workshop — one story per concept, JSDoc summaries the manifest can carry, stories run as tests, and an opt-in MCP channel that replaces guessing with querying."
domain: engineering
workspaces:
- engineering
packs:
- engineering-base
---
# storybook-workshop
A story is not a demo. It is the component's **executable documentation**: the artefact an
agent reads to learn what the component accepts, and the harness that catches a contrast or
focus defect while the component is still in isolation. Stack-agnostic — the tool is
Storybook, the discipline is not.
Lifted out of [`react-shadcn-ui`](../react-shadcn-ui/SKILL.md) § Component workshop, which
keeps a pointer here rather than a copy.
## The Iron Law
```
ONE CONCEPT PER STORY. NEVER A "SizesAndVariants" STORY.
EVERY COMPONENT EXPORT AND EVERY STORY CARRIES A JSDoc @summary.
A STORY THAT SHOWS AN ANTI-PATTERN IS TAGGED `!manifest` OR THE AGENT LEARNS FROM IT.
STORIES RUN. A STORY NOBODY RUNS IS A SCREENSHOT WITH EXTRA STEPS.
```
## When to use
A shared component library exists or is being started, and one of: stories are being added,
an agent needs an inventory of what the library already offers, an a11y regression should be
caught in isolation rather than on a screen, or a Storybook MCP channel is available and
nobody has decided whether to use it.
**Skip it** for a handful of one-off components with a single consumer — the setup and
upkeep outweigh the return, and that judgement belongs to the reader rather than to this
skill.
## The story set — one concept per story
The state-coverage rows every reusable component owes become **story names**:
`Default`, `Loading`, `Empty`, `Error`, `Disabled`, `Dark`. One concept each.
A combined story — the canonical anti-pattern is `SizesAndVariants` — fails at both jobs it
has: a human cannot tell which knob caused what, and an agent reading it cannot learn a
single prop's effect because every value is co-present. Split it, one axis per story
(Storybook's own guidance on writing effective stories).
Stories render under the **same semantic tokens** as the app and the `.dark` class; a
hardcoded preview theme makes the workshop lie about what the component looks like in place.
### The five behavioral stories — assert a value, never an image
`Hover`, `Focus`, `Active`, `Keyboard` and `ReducedMotion` join the set above, and they
carry one extra rule: **each asserts a resolved computed style or a handler effect, never a
screenshot.** A component that owes a state checklist and proves it with a pixel diff has
not proved it — a screenshot is a resting frame, and none of these five is a resting frame.
| Story | Drive | Assert |
|---|---|---|
| `Hover` | `userEvent.hover(el)` | the resolved property the hover rule is supposed to change actually changed |
| `Focus` | focus the element | the `:focus` styles resolve as intended |
| `Keyboard` | `userEvent.tab()` until the element is active | a visible `:focus-visible` indicator resolves — programmatic focus does **not** set it in Chromium, so a `.focus()` call reports a real focus ring as absent |
| `Active` | press and hold | what pressing adds **on top of hover** — a pointer cannot press without hovering, so asserting against the resting style makes one removed hover rule fail two stories |
| `ReducedMotion` | emulate `prefers-reduced-motion: reduce` | what the surface presents *instead of* the motion, per [`accessibility-auditor`](../accessibility-auditor/SKILL.md) § 2 — not that the media block exists |
The `Active` and `Keyboard` rows are not style advice. Both are measured failure modes from
building the probe that backs this set, recorded here so the next author does not rediscover
them: the first produced a duplicate finding for a single defect, the second reports a focus
ring that is present as missing.
### Responsive rows — one row per declared breakpoint
Beside the state stories, each responsive component owes a row per declared breakpoint
(1440 / 768 / 375 / 320) asserting **the layout property that is supposed to change there** —
`flex-direction`, `grid-template-columns`, `display`. One row per breakpoint, not one per
property: a single media rule usually moves several properties at once, and one finding each
counts one defect several times.
**Where a workshop is absent, this set is still assertable.** These stories are a contract
about what is asserted, not about a particular runner — the `ui_conformance_probe` script
exercises the same five states and the same breakpoint rows against a file URL. It and its
sensitivity fixture live in the package repository and are **not** shipped to consumers,
which is why both are named rather than linked: a path to either resolves nowhere in an
install. The fixture is the evidence for the two sensitivity claims above — removing one
hover rule turns exactly the hover assertion red and nothing else, and removing one media
rule turns exactly the matching breakpoint row red.
## JSDoc `@summary` is required, and the reason is mechanical
The manifest an agent reads **truncates descriptions**. A component whose summary is a
paragraph arrives as a fragment, and the agent then infers the rest. So:
```ts
/** @summary A single-action button. Owns its own focus ring; never wrap in an anchor. */
export const Button = …
/** @summary Disabled — the click handler is not attached, not merely ignored. */
export const Disabled: StoryObj<typeof Button> = …
```
One sentence, front-loaded with the fact that changes a caller's decision.
## Before authoring — review what the library already has
The first step is not a story, it is an inventory. A library large enough to want a workshop
is large enough that the component being asked for already exists under another name, and a
second `Badge` is the failure this skill's own § Do NOT is trying to prevent elsewhere.
Read the existing stories — via the MCP channel below when it is available, by reading the
files otherwise — and only then decide whether anything needs writing.
## Procedure
1. **Inspect the existing story set before authoring anything.** Read what the library
already has — via the MCP channel below when it is available, otherwise by reading the
story files — and check the component against it by name and by shape.
- **Source of truth:** the story files, or `docs-list` over a running Storybook.
- **Verify:** the component you are about to write does not already exist under another name.
2. **One story per concept**, named from the state set above.
- **Verify:** no story name joins two axes with `And`.
3. **Write the `@summary` on the export and on each story.**
- **Verify:** every export in the file has one, and each is a single sentence.
4. **Tag what must not be learned from** — see § Do NOT.
- **Verify:** every anti-pattern or deprecated story carries `tags: ['!manifest']`.
5. **Run them** — § Validate.
- **Verify:** the run reports pass/fail per story, and a11y findings land in the shape below.
## Validate — stories are tests
Run the project's story test command (the Vitest addon's `storybook test`, or whatever
`package.json` declares — read it, do not assume a name), then read the a11y result.
Write findings into `state.ui_review.a11y` in the shape
[`react-shadcn-ui`](../react-shadcn-ui/SKILL.md) § Review pass already defines:
```
{violations: [{rule, selector, severity}, …], severity_floor?, accepted_violations?}
```
**Use that shape exactly.** The engine de-duplicates on it, so a finding written in a
different shape is not a differently-formatted finding — it is a finding the engine counts
twice.
Browser tooling stays a **consumer dependency**, the same posture as `react-shadcn-ui`: this
package installs no browser and runs none. Absent the tooling, say the a11y half did not run
rather than reporting zero violations — an unrun check and a clean check produce the same
number and mean opposite things.
**One finding is decidable without a browser**, and it ships beside this skill:
`scripts/story_contrast_floor.ts` computes the WCAG 2.1 contrast ratio between two colours a
story *declares in its own args* and emits the same `(rule, selector, severity)` shape. It is
**not axe** and not a substitute for § Validate — it sees no rendered page, so role, focus,
and computed-style defects are all outside it, and a colour arriving through a token
indirection is invisible to it. It exists because "we cannot run axe here" is not a reason to
check nothing, and a declared colour pair is decidable from the file.
## Security constraints
`scripts/story_contrast_floor.ts` reads story files and computes arithmetic. No network, no
subprocess, no writes, and it never evaluates the story it reads — it matches declared literals
with a regex rather than importing the module, so running it over an untrusted repository
executes none of that repository's code. Importing a story to inspect its args would run
that file, which is the one thing a static checker must not start doing.
## MCP path — opt-in channel, never a dependency
**"Opt-in" is about the agent connecting, not about installing.** A 2026-08-24
`npx storybook@latest init --yes` scaffold ships `@storybook/addon-mcp` *and*
`@storybook/addon-a11y` in `devDependencies` by default — recorded verbatim in
`tests/fixtures/stack/storybook-current/`, which is also the only source any
Storybook major stated here may come from (`storybook@^10.5.10`). So the addon
usually already exists; what stays optional is whether an agent reaches a
running instance. The same scaffold prints *"installed but is not entirely set
up yet"*, so its presence is not evidence the channel works.
When the project has `@storybook/addon-mcp` **and** a running Storybook,
[`existing-ui-audit`](../existing-ui-audit/SKILL.md) prefers the live tools —
`docs-list`, then `docs-show` for the components that matter — over the
hand-read inventory. **The live read wins; the file read is the fallback**, and the fallback
is never removed: an agent that cannot reach a running Storybook must still be able to
inventory the library. The tool names are version-bound and were re-derived from a throwaway
installation on 2026-09-13 — [`existing-ui-audit`](../existing-ui-audit/SKILL.md) § 4b carries
the derivation and the version boundary; do not restate it here.
**React-only while in preview.** Storybook's own MCP FAQ (docs 10.5) states the documentation
toolset supports React only during preview, so Vue, Angular, and Web Components take the
file-read path. Stated here rather than discovered at runtime.
Two operative rules, restated from Storybook's suggested agent instructions rather than
copied verbatim:
- **Never use an undocumented prop.** If the manifest does not carry it, it is not part of
the component's surface — reading it out of the implementation is how a private prop
becomes a de-facto API.
- **Fetch the story instructions before writing a story.** The project's own conventions
live there; writing first and reconciling later produces a story that matches nothing.
## Output format
1. Name the inventory source used — MCP or file read — and why.
2. List the stories authored or changed, one line each, with their concept.
3. Report the story-test result, and the a11y findings in the `(rule, selector, severity)`
shape.
4. State explicitly what did not run (no browser tooling, no running Storybook, non-React
renderer).
## Gotchas
- **`SizesAndVariants` looks efficient and destroys both jobs** — a human cannot attribute
the effect, and an agent cannot learn a single prop from it.
- **A truncated description reads as a complete one.** The manifest cuts the tail without
marking it, so a paragraph-long summary arrives as a confident fragment.
- **A stale story is worse than no story.** It props-drills values the component no longer
accepts and documents an API that does not exist; keep stories beside the component and
change them in the same commit.
- **An unrun a11y check reports zero violations** — the same number a clean run reports.
Say which one happened.
- **The MCP channel disappearing is normal**, not an error: no running Storybook is the
fallback's whole purpose.
## Do NOT
- Do NOT combine two axes in one story.
- Do NOT ship a component export or a story without a `@summary`.
- Do NOT leave an anti-pattern or deprecated story untagged: it carries
`tags: ['!manifest']` so it is excluded from the manifest and the agent never learns from
it (Storybook's own tag mechanism for curating what the manifest exposes).
- Do NOT hardcode a preview theme in a story.
- Do NOT make the MCP channel required, and do NOT remove the file-read fallback.
- Do NOT report zero a11y violations when the a11y tooling did not run.
## See also
- [`react-shadcn-ui`](../react-shadcn-ui/SKILL.md) — the React stack lane, and the `(rule, selector, severity)` shape § Validate writes into.
- [`existing-ui-audit`](../existing-ui-audit/SKILL.md) — the inventory the MCP path accelerates.
- [`js-library-packaging`](../js-library-packaging/SKILL.md) — the package around the workshop.
- [`accessibility-auditor`](../accessibility-auditor/SKILL.md) — the WCAG depth behind the a11y half.
- [`fe-design`](../fe-design/SKILL.md) § Component Architecture — the stack-agnostic isolate-and-document principle.