视觉画布设计
canvas-design
基于设计原则创作静态视觉作品,用于海报与品牌图形。
frontend-storybook【文档与办公】Use this skill when the user says 'storybook', 'component documentation', 'visual testing', 'storybook addon', 'CSF', 'component story'. This skill enforces CSF (Component Story Format) 3.x standards, accessible stories, interaction testing, and addon integration. Applies to any frontend stack.
独立 GitHub 快照 · 自动分类 · 以原作者文档为准
Create, maintain, and optimize Storybook stories using CSF 3.x format with interaction tests, accessibility checks, and addon-driven workflows. Covers setup across React/Vue/Angular/Svelte, visual testing via Chromatic, custom theming, and documentation mode.
Exact phrases: "write stories", "storybook", "CSF", "component story", "storybook addon", "visual test", "story for", "stories for", "storybook setup", "interaction test", "chromatic", "storybook docs", "storybook theme", "storybook decorator"
.storybook/main.js or main.ts to detect existing Storybook config.storybook/main.js under addons arrayautodocs is enabled globally or per-componentNo file output unless requested.
.stories.tsx/.stories.ts format (default export with meta, named exports for stories)import statements and meta object with component, title, tags, argTypesplay function from @storybook/testconst meta = { component } satisfies Meta<typeof Component> with named story exports@storybook/addon-a11y included in at least one story per componentplay function for any component with user input or animationsparameters.viewport to test mobile/tablet/desktop@storybook/addon-docs) configured with autodocs for the componentargs mapping for story variations120 lines per component story set.
How many components?
|-- 1-10 components -->
| Simple structure: Components/Button, Components/Card
| Co-locate stories with components (Button.stories.tsx next to Button.tsx)
|
|-- 10-50 components -->
| Feature-based grouping: Feature/Orders/OrderList, Feature/Orders/OrderCard
| Group by domain, not by type
|
|-- 50+ components (design system) -->
Nested structure: DesignSystem/Buttons/Primary, DesignSystem/Buttons/Secondary
Each variant gets its own story
Atomic structure: Atoms, Molecules, Organisms
What states does the component have?
|-- Default / empty state -->
| STORY REQUIRED: shows component with no data
| Example: empty list, empty card, placeholder text
|
|-- Loading state -->
| STORY REQUIRED: skeleton or spinner
| Example: SkeletonList, LoadingButton
|
|-- Error state -->
| STORY REQUIRED: error message, retry action
| Example: ErrorCard, ErrorForm
|
|-- Edge cases -->
| STORY REQUIRED: long text, missing props, zero data, overflow
| Example: VeryLongText, MissingImage, SingleItem
|
|-- Interactive behavior -->
| STORY REQUIRED if component has user input
| Use play() function to simulate clicks, typing, hover
|
|-- Responsive variants -->
STORY REQUIRED: mobile (375px), tablet (768px), desktop (1280px)
Use parameters.viewport
npx storybook@latest init --type react # or vue3, angular, nextjs, sveltekit
Check .storybook/main.ts for required addons:
| Addon | Package | Purpose |
|---|---|---|
| Essentials | @storybook/addon-essentials | Docs, Controls, Actions, Viewport, Backgrounds |
| Interactions | @storybook/addon-interactions | Play function testing |
| Accessibility | @storybook/addon-a11y | aXe-based accessibility audits |
| Themes | @storybook/addon-themes | Theme switching toolbar |
| Designs | @storybook/addon-designs | Figma design embeds |
// .storybook/preview.ts
const preview: Preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: { matchers: { color: /(background|color)$/i, date: /Date$/ } },
viewport: { viewports: NEW_VIEWPORTS },
a11y: { config: { rules: [{ id: 'color-contrast', enabled: true }] } },
},
decorators: [withThemeFromJSXProvider({ themes: { light, dark }, defaultTheme: 'light', Provider: ThemeProvider })],
tags: ['autodocs'],
};
const meta = {
title: 'Components/Button',
component: Button,
tags: ['autodocs'],
argTypes: {
variant: { control: 'select', options: ['primary', 'secondary', 'ghost'] },
size: { control: 'select', options: ['sm', 'md', 'lg'] },
disabled: { control: 'boolean' },
onClick: { action: 'clicked' },
},
} satisfies Meta<typeof Button>;
export const Primary: Story = { args: { variant: 'primary', children: 'Click Me' } };
export const Disabled: Story = { args: { ...Primary.args, disabled: true } };
export const Loading: Story = { args: { ...Primary.args, loading: true } };
import { userEvent, within, expect, fn } from '@storybook/test';
export const Interactive: Story = {
args: { onClick: fn() },
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
await userEvent.click(canvas.getByRole('button'));
expect(args.onClick).toHaveBeenCalled();
},
};
npm i -D chromatic
npx chromatic --project-token=<token>
# .github/workflows/chromatic.yml
- run: npx chromatic --project-token=${{ secrets.CHROMATIC_TOKEN }}
Run a11y plugin scan in Storybook UI or via CLI:
npx test-storybook --coverage
Fix violations: missing ARIA labels, insufficient color contrast, missing focus indicators.
Enable autodocs: 'tag' in main.ts. Add tags: ['autodocs'] to each meta. For custom docs:
// Button.docs.mdx
import { Meta, Story, Canvas, Controls } from '@storybook/blocks';
<Meta of={ButtonStories} />
<Canvas><Story of={ButtonStories.Primary} /></Canvas>
<Controls of={ButtonStories.Primary} />
import { http, HttpResponse } from 'msw'
import { initialize, mswLoader } from 'msw-storybook-addon'
initialize()
const meta = {
component: OrderList,
loaders: [mswLoader],
} satisfies Meta<typeof OrderList>
export const WithOrders: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/orders', () => {
return HttpResponse.json([{ id: '1', name: 'Order 1', total: 100 }])
}),
],
},
},
}
export const Empty: Story = {
parameters: {
msw: {
handlers: [
http.get('/api/orders', () => {
return HttpResponse.json([])
}),
],
},
},
}
storiesOf API: Always use CSF 3.x. Never use storiesOf — removed in Storybook 8.parameters or decorators in meta — never import from '@storybook/addon-*'.autodocs tag: Without tags: ['autodocs'] or global enable, docs tab won't generate.any type for props: Derive from component's props type. Use StoryObj<typeof meta>.parameters.viewport with named viewports from config.| Feature | Storybook | Ladle | Histoire | Catalog |
|---|---|---|---|---|
| Framework support | React, Vue, Angular, Svelte, Web Components | React only | Vue only | Any (static) |
| CSF 3.x | Yes | Yes | No | N/A |
| Interaction testing | Built-in | Limited | Limited | No |
| A11y addon | Yes | No | No | No |
| Visual testing (Chromatic) | Built-in | Third-party | Third-party | No |
| Bundle size (SB) | Large | Small | Small | Minimal |
| MSW integration | Yes | Yes | No | No |
// .storybook/preview.ts — theme switching
import { withThemeFromJSXProvider } from '@storybook/addon-themes';
import { ThemeProvider } from '../src/lib/ThemeProvider';
import { lightTheme, darkTheme } from '../src/lib/themes';
export const decorators = [
withThemeFromJSXProvider({
themes: {
light: lightTheme,
dark: darkTheme,
},
defaultTheme: 'light',
Provider: ThemeProvider,
GlobalStyles: () => null,
}),
];
// Composing stories from other stories
import { Primary as ButtonPrimary } from '../Button/Button.stories';
const meta = {
title: 'Components/Dialog',
component: Dialog,
} satisfies Meta<typeof Dialog>;
// Reuse Button story inside Dialog story
export const WithButton: Story = {
args: {
title: 'Confirm',
children: <Button {...ButtonPrimary.args}>OK</Button>,
onClose: fn(),
},
};
// Args composition for complex components
export const WithForm: Story = {
args: {
...Default.args,
children: (
<Form>
<TextField label="Name" />
<Button {...ButtonPrimary.args}>Submit</Button>
</Form>
),
},
};
// Theming with design tokens
// .storybook/preview.ts
import { globalTypes } from '@storybook/addons';
export const globalTypes = {
theme: {
name: 'Theme',
description: 'Global theme for components',
defaultValue: 'light',
toolbar: {
icon: 'circlehue',
items: ['light', 'dark', 'high-contrast'],
showName: true,
dynamicTitle: true,
},
},
locale: {
name: 'Locale',
description: 'Internationalization locale',
defaultValue: 'en-US',
toolbar: {
icon: 'globe',
items: ['en-US', 'de-DE', 'vi-VN', 'ja-JP'],
showName: true,
},
},
};
// With Figma design specs
export const ButtonStory = {
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/.../Button',
},
},
};
# .github/workflows/chromatic.yml
name: Chromatic
on: [pull_request]
jobs:
chromatic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for Chromatic to detect changed stories
- run: npm ci
- uses: chromaui/action@v1
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
onlyChanged: true # Only snapshot changed stories
exitOnceUploaded: true
buildScriptName: build-storybook
# Auto-accept changes from dependabot
autoAcceptChanges: 'dependabot/**'
# Prevent false positives for specific branches
skip: '@(renovate/**|docs/**)'
// Chromatic mode: only globally accessible styles
// Prevent Chromatic from crashing on theme provider issues
export const decorators = [
(Story) => (
<div style={{ padding: '1rem', fontFamily: 'sans-serif' }}>
<Story />
</div>
),
];
// .storybook/test-runner.ts
// Custom test runner for a11y and interaction testing
import { getStoryContext, type TestRunnerConfig } from '@storybook/test-runner';
import { checkA11y, injectAxe } from 'axe-playwright';
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page, context) {
const storyContext = await getStoryContext(page, context);
// Skip a11y tests for stories that opt out
if (storyContext.parameters?.a11y?.disable) return;
// Run aXe and fail on critical violations
const results = await checkA11y(page, 'storybook-root', {
detailedReport: false,
includedImpacts: ['critical', 'serious'],
});
expect(results).toHaveNoViolations();
},
};
export default config;
storyStoreV7: true for lazy compilation@storybook/test package is small (~3KB) and only used in story filesstoryStoreV7: true reduces dev startup time by 60% for large librariesonlyChanged: true reduces snapshot time by 80%+ on typical PRs@storybook/addon-a11y runs aXe on every story automatically — fix violations before merging// Accessibility-focused stories
export const A11yViolations: Story = {
name: 'Accessibility Check',
parameters: {
a11y: {
config: { rules: [{ id: 'color-contrast', enabled: true }] },
element: '#storybook-root',
},
},
play: async ({ canvasElement }) => {
const results = await axe(canvasElement);
expect(results).toHaveNoViolations();
},
};
| Anti-Pattern | Problem | Fix |
|---|---|---|
| No stories for empty/loading states | Missing test coverage for edge cases | Add stories for every component state |
| Stories that make real API calls | Flaky, slow, require network | Use MSW to mock all API calls |
Using storiesOf API (CSF 2.x) | Deprecated, removed in SB 8 | Use CSF 3.x const meta = { ... } |
| Hardcoded viewports in stories | Inconsistent across components | Use parameters.viewport with named viewports |
| Skipping a11y addon | Accessibility issues caught late | Add @storybook/addon-a11y to every project |
| Too many stories per component | Maintenance burden | Cover: default, loading, error, edge cases. Not every prop combination |
Missing autodocs tag | Docs tab not generated | Add tags: ['autodocs'] to meta |
storiesOf API@storybook/addon-* directly in story files — use parameters or decorators in the meta exporttags: ['autodocs'] in meta to generate documentation automaticallyany type for story props — derive from the component's props typeButton.stories.tsx next to Button.tsx) unless the project centralizes themplay with await — missing await = flaky testsonlyChanged: true in CI to minimize snapshot timeNo artifact produced unless requested.
Next skill: frontend-pwa (if the component needs offline support or a service worker)
Carry forward: Component prop types, existing story examples, addon list from config
` interface EventObserver { onEvent(event: T): Promise; }
class EventBus { private observers: Set<EventObserver> = new Set(); subscribe(observer: EventObserver): void { this.observers.add(observer); } unsubscribe(observer: EventObserver): void { this.observers.delete(observer); } async emit(event: T): Promise { const results = Array.from(this.observers).map(o => o.onEvent(event)); await Promise.allSettled(results); } } `
config: defaults: timeout: 30s retryCount: 3 overrides: production: timeout: 60s retryCount: 5 development: timeout: 300s retryCount: 1
| Metric | Threshold | Severity | Action |
|---|---|---|---|
| Error rate | > 1% over 5min | Critical | Page on-call |
| p99 latency | > 2s over 5min | Warning | Investigate |
| Throughput drop | > 50% over 1min | Critical | Check upstream |
| Queue depth | > 1000 over 1min | Warning | Scale consumers |
| Disk usage | > 85% | Warning | Clean or expand |
| Memory usage | > 90% heap | Critical | Restart or scale |
| Anti-Pattern | Symptom | Root Cause | Solution |
|---|---|---|---|
| Premature optimization | Complex code for no measured benefit | Guessing instead of profiling | Measure first, optimize based on data |
| Copy-paste reuse | Duplicate code across codebase | Lack of abstraction | Extract shared logic into libraries |
| Gold-plating | Features with no current requirement | Over-engineering | YAGNI — build what's needed now |
| Magical thinking | Assumptions without validation | Skipping error handling | Handle all failure modes explicitly |
Cache hierarchy: L1 (in-memory local) → L2 (distributed Redis/Memcached) → L3 (CDN/Edge). Cache invalidation: TTL-based (simple, stale), event-based (complex, fresh), write-through (consistent, higher write latency), write-behind (fast writes, eventual consistency).
Who is the primary audience? ├── Developers → Component API docs, code snippets, prop tables ├── Designers → Design system preview, design token values, variants └── PM/QA → Interaction tests, visual regression reports, state coverage What level of documentation is needed? ├── Basic → Props table + default story ├── Standard → All states + accessibility report + code snippet └── Comprehensive → Design guidelines + usage rules + migration guide
What type of validation is needed? ├── Visual → Chromatic/Percy for screenshot comparison per PR ├── Accessibility → axe-core on every story, fail CI on violations └── Interaction → Playwright/Cypress component tests for user flows How often do stories change? ├── Every PR → Automate visual + a11y tests in CI pipeline └── Infrequent → Manual review + scheduled snapshot updates