storybook-setup
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseStorybook Setup
Storybook 环境搭建
Configure Storybook for comprehensive component documentation and testing.
配置Storybook以实现全面的组件文档管理与测试。
Core Workflow
核心工作流程
- Initialize Storybook: Setup with framework
- Configure addons: Controls, actions, a11y
- Write stories: Document components
- Add documentation: MDX pages
- Setup testing: Visual regression
- Deploy docs: Static hosting
- 初始化Storybook:根据对应框架完成搭建
- 配置插件:添加控件、交互动作、无障碍(a11y)等插件
- 编写组件故事:为组件创建文档化示例
- 添加文档内容:编写MDX格式的说明文档
- 配置测试:设置视觉回归检测
- 部署文档:静态托管发布
Installation
安装步骤
bash
undefinedbash
undefinedInitialize Storybook
初始化Storybook
npx storybook@latest init
npx storybook@latest init
Or with specific framework
或针对特定框架初始化
npx storybook@latest init --type react
npx storybook@latest init --type nextjs
npx storybook@latest init --type vue3
undefinednpx storybook@latest init --type react
npx storybook@latest init --type nextjs
npx storybook@latest init --type vue3
undefinedConfiguration
配置说明
Main Configuration
主配置文件
typescript
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-designs',
'@storybook/addon-coverage',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
typescript: {
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
viteFinal: async (config) => {
// Customize Vite config
return config;
},
};
export default config;typescript
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/react-vite';
const config: StorybookConfig = {
stories: [
'../src/**/*.mdx',
'../src/**/*.stories.@(js|jsx|mjs|ts|tsx)',
],
addons: [
'@storybook/addon-onboarding',
'@storybook/addon-links',
'@storybook/addon-essentials',
'@chromatic-com/storybook',
'@storybook/addon-interactions',
'@storybook/addon-a11y',
'@storybook/addon-designs',
'@storybook/addon-coverage',
],
framework: {
name: '@storybook/react-vite',
options: {},
},
docs: {
autodocs: 'tag',
},
staticDirs: ['../public'],
typescript: {
reactDocgen: 'react-docgen-typescript',
reactDocgenTypescriptOptions: {
shouldExtractLiteralValuesFromEnum: true,
shouldRemoveUndefinedFromOptional: true,
propFilter: (prop) =>
prop.parent ? !/node_modules/.test(prop.parent.fileName) : true,
},
},
viteFinal: async (config) => {
// 自定义Vite配置
return config;
},
};
export default config;Preview Configuration
预览配置文件
typescript
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import { themes } from '@storybook/theming';
import '../src/styles/globals.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
{ name: 'gray', value: '#f5f5f5' },
],
},
layout: 'centered',
docs: {
theme: themes.light,
},
a11y: {
config: {
rules: [
{ id: 'color-contrast', enabled: true },
{ id: 'label', enabled: true },
],
},
},
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: { width: '375px', height: '667px' },
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
},
desktop: {
name: 'Desktop',
styles: { width: '1440px', height: '900px' },
},
},
},
},
globalTypes: {
theme: {
description: 'Global theme',
defaultValue: 'light',
toolbar: {
title: 'Theme',
icon: 'circlehollow',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
decorators: [
(Story, context) => {
const theme = context.globals.theme;
return (
<div className={theme === 'dark' ? 'dark' : ''}>
<div className="bg-white dark:bg-gray-900 p-4">
<Story />
</div>
</div>
);
},
],
};
export default preview;typescript
// .storybook/preview.ts
import type { Preview } from '@storybook/react';
import { themes } from '@storybook/theming';
import '../src/styles/globals.css';
const preview: Preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
backgrounds: {
default: 'light',
values: [
{ name: 'light', value: '#ffffff' },
{ name: 'dark', value: '#1a1a1a' },
{ name: 'gray', value: '#f5f5f5' },
],
},
layout: 'centered',
docs: {
theme: themes.light,
},
a11y: {
config: {
rules: [
{ id: 'color-contrast', enabled: true },
{ id: 'label', enabled: true },
],
},
},
viewport: {
viewports: {
mobile: {
name: 'Mobile',
styles: { width: '375px', height: '667px' },
},
tablet: {
name: 'Tablet',
styles: { width: '768px', height: '1024px' },
},
desktop: {
name: 'Desktop',
styles: { width: '1440px', height: '900px' },
},
},
},
},
globalTypes: {
theme: {
description: '全局主题',
defaultValue: 'light',
toolbar: {
title: '主题',
icon: 'circlehollow',
items: ['light', 'dark'],
dynamicTitle: true,
},
},
},
decorators: [
(Story, context) => {
const theme = context.globals.theme;
return (
<div className={theme === 'dark' ? 'dark' : ''}>
<div className="bg-white dark:bg-gray-900 p-4">
<Story />
</div>
</div>
);
},
],
};
export default preview;Writing Stories
编写组件故事
Component Story Format (CSF3)
组件故事格式(CSF3)
typescript
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: 'A versatile button component with multiple variants and sizes.',
},
},
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost'],
description: 'Visual style variant',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'primary' },
},
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg'],
description: 'Button size',
},
disabled: {
control: 'boolean',
description: 'Disable the button',
},
loading: {
control: 'boolean',
description: 'Show loading state',
},
children: {
control: 'text',
description: 'Button content',
},
},
args: {
onClick: fn(),
children: 'Button',
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// Basic stories
export const Primary: Story = {
args: {
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
},
};
export const Outline: Story = {
args: {
variant: 'outline',
},
};
export const Ghost: Story = {
args: {
variant: 'ghost',
},
};
// Size variants
export const Small: Story = {
args: {
size: 'sm',
},
};
export const Large: Story = {
args: {
size: 'lg',
},
};
// States
export const Disabled: Story = {
args: {
disabled: true,
},
};
export const Loading: Story = {
args: {
loading: true,
},
};
// With icons
export const WithIcon: Story = {
args: {
children: (
<>
<PlusIcon className="mr-2 h-4 w-4" />
Add Item
</>
),
},
};
// All variants showcase
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};typescript
// src/components/Button/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { fn } from '@storybook/test';
import { Button } from './Button';
const meta = {
title: 'Components/Button',
component: Button,
parameters: {
layout: 'centered',
docs: {
description: {
component: '一款支持多种样式变体与尺寸的通用按钮组件。',
},
},
},
tags: ['autodocs'],
argTypes: {
variant: {
control: 'select',
options: ['primary', 'secondary', 'outline', 'ghost'],
description: '视觉样式变体',
table: {
type: { summary: 'string' },
defaultValue: { summary: 'primary' },
},
},
size: {
control: 'radio',
options: ['sm', 'md', 'lg'],
description: '按钮尺寸',
},
disabled: {
control: 'boolean',
description: '禁用按钮',
},
loading: {
control: 'boolean',
description: '显示加载状态',
},
children: {
control: 'text',
description: '按钮内容',
},
},
args: {
onClick: fn(),
children: 'Button',
},
} satisfies Meta<typeof Button>;
export default meta;
type Story = StoryObj<typeof meta>;
// 基础样式示例
export const Primary: Story = {
args: {
variant: 'primary',
},
};
export const Secondary: Story = {
args: {
variant: 'secondary',
},
};
export const Outline: Story = {
args: {
variant: 'outline',
},
};
export const Ghost: Story = {
args: {
variant: 'ghost',
},
};
// 尺寸变体示例
export const Small: Story = {
args: {
size: 'sm',
},
};
export const Large: Story = {
args: {
size: 'lg',
},
};
// 状态示例
export const Disabled: Story = {
args: {
disabled: true,
},
};
export const Loading: Story = {
args: {
loading: true,
},
};
// 带图标示例
export const WithIcon: Story = {
args: {
children: (
<>
<PlusIcon className="mr-2 h-4 w-4" />
添加项目
</>
),
},
};
// 全变体展示
export const AllVariants: Story = {
render: () => (
<div className="flex flex-wrap gap-4">
<Button variant="primary">Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="outline">Outline</Button>
<Button variant="ghost">Ghost</Button>
</div>
),
};Interactive Stories
交互式组件故事
typescript
// src/components/Form/Form.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect, fn } from '@storybook/test';
import { Form } from './Form';
const meta: Meta<typeof Form> = {
title: 'Components/Form',
component: Form,
args: {
onSubmit: fn(),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const FilledForm: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// Fill out the form
const emailInput = canvas.getByLabelText('Email');
await userEvent.type(emailInput, 'test@example.com', { delay: 50 });
const passwordInput = canvas.getByLabelText('Password');
await userEvent.type(passwordInput, 'password123', { delay: 50 });
// Submit the form
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);
// Assert the form was submitted
await expect(args.onSubmit).toHaveBeenCalled();
},
};
export const ValidationError: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// Submit without filling
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);
// Check for error messages
await expect(canvas.getByText('Email is required')).toBeInTheDocument();
},
};typescript
// src/components/Form/Form.stories.tsx
import type { Meta, StoryObj } from '@storybook/react';
import { within, userEvent, expect, fn } from '@storybook/test';
import { Form } from './Form';
const meta: Meta<typeof Form> = {
title: 'Components/Form',
component: Form,
args: {
onSubmit: fn(),
},
};
export default meta;
type Story = StoryObj<typeof meta>;
export const FilledForm: Story = {
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
// 填写表单
const emailInput = canvas.getByLabelText('Email');
await userEvent.type(emailInput, 'test@example.com', { delay: 50 });
const passwordInput = canvas.getByLabelText('Password');
await userEvent.type(passwordInput, 'password123', { delay: 50 });
// 提交表单
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);
// 验证表单已提交
await expect(args.onSubmit).toHaveBeenCalled();
},
};
export const ValidationError: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
// 未填写内容直接提交
const submitButton = canvas.getByRole('button', { name: /submit/i });
await userEvent.click(submitButton);
// 检查错误提示
await expect(canvas.getByText('Email is required')).toBeInTheDocument();
},
};MDX Documentation
MDX格式文档
mdx
{/* src/components/Button/Button.mdx */}
import { Meta, Story, Canvas, Controls, ArgTypes } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
import { Button } from './Button';
<Meta of={ButtonStories} />mdx
{/* src/components/Button/Button.mdx */}
import { Meta, Story, Canvas, Controls, ArgTypes } from '@storybook/blocks';
import * as ButtonStories from './Button.stories';
import { Button } from './Button';
<Meta of={ButtonStories} />Button
按钮组件
The Button component is used to trigger actions or navigation.
按钮组件用于触发操作或页面导航。
Import
导入方式
tsx
import { Button } from '@/components/Button';tsx
import { Button } from '@/components/Button';Usage
使用示例
<Canvas of={ButtonStories.Primary} />
<Canvas of={ButtonStories.Primary} />
Variants
样式变体
Buttons come in four variants to communicate different levels of emphasis.
<Canvas>
<Story of={ButtonStories.AllVariants} />
</Canvas>按钮提供四种样式变体,以区分操作的重要程度。
<Canvas>
<Story of={ButtonStories.AllVariants} />
</Canvas>Primary
主按钮
Use for primary actions that are the main call to action on a page.
用于页面上的核心操作,是主要的调用动作。
Secondary
次按钮
Use for secondary actions that complement the primary action.
用于辅助主按钮的次要操作。
Outline
轮廓按钮
Use for tertiary actions or when you want less visual emphasis.
用于三级操作或需要弱化视觉强调的场景。
Ghost
幽灵按钮
Use for navigation or very subtle actions.
用于导航或非常低调的操作场景。
Sizes
尺寸选项
<Canvas>
<div className="flex items-center gap-4">
<Button size="sm">Small</Button>
<Button size="md">Medium</Button>
<Button size="lg">Large</Button>
</div>
</Canvas>
<Canvas>
<div className="flex items-center gap-4">
<Button size="sm">小尺寸</Button>
<Button size="md">中尺寸</Button>
<Button size="lg">大尺寸</Button>
</div>
</Canvas>
States
状态示例
Disabled
禁用状态
<Canvas of={ButtonStories.Disabled} />
<Canvas of={ButtonStories.Disabled} />
Loading
加载状态
<Canvas of={ButtonStories.Loading} />
<Canvas of={ButtonStories.Loading} />
Props
属性说明
<ArgTypes of={Button} />
<ArgTypes of={Button} />
Accessibility
无障碍支持
- Buttons use the native element
<button> - Loading state announces to screen readers
- Focus states are clearly visible
- Disabled buttons maintain proper ARIA attributes
- 按钮使用原生元素
<button> - 加载状态会向屏幕阅读器发送通知
- 焦点状态清晰可见
- 禁用按钮保留正确的ARIA属性
Design Guidelines
设计规范
- Use descriptive button text
- Limit to one primary button per section
- Keep button text concise (2-4 words)
- Use icons to reinforce meaning, not replace text
undefined- 使用描述性的按钮文字
- 每个区域最多保留一个主按钮
- 按钮文字简洁(2-4个词)
- 使用图标强化含义,而非替代文字
undefinedTesting Integration
测试集成
Visual Regression with Chromatic
基于Chromatic的视觉回归检测
typescript
// .storybook/main.ts
const config: StorybookConfig = {
addons: [
'@chromatic-com/storybook',
],
};json
// package.json
{
"scripts": {
"chromatic": "chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
}
}typescript
// .storybook/main.ts
const config: StorybookConfig = {
addons: [
'@chromatic-com/storybook',
],
};json
// package.json
{
"scripts": {
"chromatic": "chromatic --project-token=$CHROMATIC_PROJECT_TOKEN"
}
}Test Runner
测试运行器
bash
npm install @storybook/test-runner -Dtypescript
// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
import { injectAxe, checkA11y } from 'axe-playwright';
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page, context) {
// Run accessibility tests
const storyContext = await getStoryContext(page, context);
if (!storyContext.parameters?.a11y?.disable) {
await checkA11y(page, '#storybook-root', {
detailedReport: true,
detailedReportOptions: { html: true },
});
}
},
};
export default config;json
// package.json
{
"scripts": {
"test-storybook": "test-storybook",
"test-storybook:ci": "test-storybook --ci"
}
}bash
npm install @storybook/test-runner -Dtypescript
// .storybook/test-runner.ts
import type { TestRunnerConfig } from '@storybook/test-runner';
import { getStoryContext } from '@storybook/test-runner';
import { injectAxe, checkA11y } from 'axe-playwright';
const config: TestRunnerConfig = {
async preVisit(page) {
await injectAxe(page);
},
async postVisit(page, context) {
// 运行无障碍测试
const storyContext = await getStoryContext(page, context);
if (!storyContext.parameters?.a11y?.disable) {
await checkA11y(page, '#storybook-root', {
detailedReport: true,
detailedReportOptions: { html: true },
});
}
},
};
export default config;json
// package.json
{
"scripts": {
"test-storybook": "test-storybook",
"test-storybook:ci": "test-storybook --ci"
}
}Design Tokens Integration
设计令牌集成
typescript
// .storybook/preview.ts
import { ThemeProvider } from 'styled-components';
import { theme } from '../src/styles/theme';
const preview: Preview = {
decorators: [
(Story) => (
<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
),
],
};typescript
// src/styles/theme.ts (document in Storybook)
export const theme = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
radii: {
sm: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
full: '9999px',
},
};typescript
// .storybook/preview.ts
import { ThemeProvider } from 'styled-components';
import { theme } from '../src/styles/theme';
const preview: Preview = {
decorators: [
(Story) => (
<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
),
],
};typescript
// src/styles/theme.ts(在Storybook中文档化)
export const theme = {
colors: {
primary: {
50: '#eff6ff',
500: '#3b82f6',
900: '#1e3a8a',
},
},
spacing: {
xs: '0.25rem',
sm: '0.5rem',
md: '1rem',
lg: '1.5rem',
xl: '2rem',
},
radii: {
sm: '0.25rem',
md: '0.375rem',
lg: '0.5rem',
full: '9999px',
},
};Publishing
发布部署
Static Export
静态导出
json
// package.json
{
"scripts": {
"build-storybook": "storybook build",
"storybook:serve": "npx http-server storybook-static"
}
}json
// package.json
{
"scripts": {
"build-storybook": "storybook build",
"storybook:serve": "npx http-server storybook-static"
}
}GitHub Pages
GitHub Pages部署
yaml
undefinedyaml
undefined.github/workflows/storybook.yml
.github/workflows/storybook.yml
name: Deploy Storybook
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build-storybook
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-staticundefinedname: 部署Storybook
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build-storybook
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./storybook-staticundefinedBest Practices
最佳实践
- One component per file: Clear organization
- Use autodocs: Generate documentation
- Add controls: Interactive exploration
- Include a11y addon: Accessibility testing
- Write play functions: Interactive tests
- Document with MDX: Rich documentation
- Use decorators: Consistent context
- Visual regression: Catch UI changes
- 单组件单文件:保持清晰的组织结构
- 使用自动文档:自动生成组件文档
- 添加交互控件:支持交互式探索组件属性
- 集成无障碍插件:进行无障碍测试
- 编写播放函数:创建交互式测试用例
- 使用MDX文档:编写丰富的说明内容
- 使用装饰器:提供一致的渲染上下文
- 视觉回归检测:及时发现UI变更问题
Output Checklist
输出检查清单
Every Storybook setup should include:
- Main configuration with addons
- Preview with global decorators
- Stories in CSF3 format
- Autodocs enabled
- Controls for all props
- Accessibility addon
- Dark mode support
- Viewport presets
- MDX documentation
- Test runner setup
- CI deployment
- Static build script
每个Storybook搭建项目应包含:
- 包含插件配置的主配置文件
- 带有全局装饰器的预览配置
- CSF3格式的组件故事
- 启用自动文档功能
- 为所有属性添加交互控件
- 无障碍测试插件
- 深色模式支持
- 预设视口尺寸
- MDX格式的说明文档
- 测试运行器配置
- CI自动化部署
- 静态构建脚本