Storybook CSF 实战:为 Checkbox 组件编写「Unchecked」状态 Story(CSF 3 / Svelte CSF / CSF Next 全框架对照)
本文围绕 Storybook 官方文档代码片段 docs/_snippets/checkbox-story-csf.md 展开。该片段本身并非散文式教程,而是一份「可被文档站点多框架切换渲染」的对照式代码示例集:它用 16 段代码覆盖 Angular、Svelte、React、Vue、Web Components 等渲染器,演示在 Component Story Format(CSF)下为一个 Checkbox 组件定义名为 Unchecked 的默认状态 Story,并用 args 传入 label: 'Unchecked' 作为组件输入。通过逐段拆解这些示例,你将掌握 CSF 3 的对象式 Story、Svelte CSF 的 defineMeta/Story 写法、Web Components 的标签字符串引用,以及实验性 CSF Next 的 preview.meta() API,并能把这些 Story 无缝嵌入 MDX 组件文档中渲染。
该代码片段在文档体系中的位置
在 Storybook 仓库中,docs/_snippets/ 目录存放的是所有教程页共用的「可切换代码片段」。它们由文档站点的 CodeSnippets 组件加载,并根据当前阅读者选择的框架、语言与写法选项卡,只渲染对应的那一段代码。
这一份 checkbox-story-csf.md 是 MDX 教程页 的主角之一。该页第 15 行起以 Checkbox.mdx 为例讲解「用 Markdown + JSX 写组件文档」,第 17 行引用 checkbox-story.md(展示 MDX 文件本体),第 21 行引用我们讨论的 checkbox-story-csf.md,并说明它承载的是被 MDX 引用的、以 CSF 编写的 Checkbox.stories.js|ts 故事文件:
This MDX file references a story file,
Checkbox.stories.js|ts, that is written in Component Story Format (CSF).
换句话说:CSF 负责定义"组件有哪些状态",MDX 负责把状态编排成可读的文档。仓库中另一个片段 checkbox-story-grouped.md 展示了同系列示例如何通过 title: 'Design System/Atoms/Checkbox' 进行分组与层级命名,可一并对照阅读。
CSF 3:用「默认导出 + 具名导出」描述组件元数据与 Story
CSF 是官方推荐的故事编写格式,本质是一个基于 ES6 模块的开放标准(详见 docs/api/csf/index.mdx)。一个 CSF 故事文件由两部分构成:
- 默认导出(default export):描述组件元数据,至少包含
component字段(Addons 依赖它生成属性表、展示组件元信息),可选title、decorators、parameters等; - 具名导出(named exports):默认情况下每一个具名导出都是一个 Story 对象。
以最常见的 CSF 3 写法为例,下面是用 JavaScript 编写的通用版本(同一文件既可用于 .js 也可用于 .jsx,覆盖 React 等 JSX 生态):
// Checkbox.stories.js|jsx —— CSF 3(common)
import { Checkbox } from './Checkbox';
export default {
component: Checkbox,
};
export const Unchecked = {
args: {
label: 'Unchecked',
},
};
要点拆解:
export default { component: Checkbox }声明「本文件围绕 Checkbox 编写」,Story 将默认渲染该组件并把args作为输入分发给它;export const Unchecked = { args: {...} }声明一个名为Unchecked的 Story 对象。与 CSF 2 的函数式Unchecked.bind({})不同,CSF 3 中具名导出是对象,因此可以放心地用 JS 展开运算符复用其上的所有注解;args是 Storybook 6.0 起引入的「具名输入」。组件的外显差异(选中/未选中、主按钮/次按钮)通常只需不同的args就能表达,本例中的label: 'Unchecked'会作为属性传给组件;- 若未显式指定,Story 在侧边栏的显示名由具名导出经
startCase规则转换而来:Unchecked→Unchecked。官方建议具名导出一律使用 UpperCamelCase(详见 docs/api/csf/index.mdx 中导出标识符与显示名的映射表)。
TypeScript 版本:satisfies + StoryObj
在 TS/TSX 项目中,推荐用 satisfies 让编辑器同时做类型收窄与推断,并借助 StoryObj 获得自动补全:
// Checkbox.stories.ts|tsx —— CSF 3(common)
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { Checkbox } from './Checkbox';
const meta = {
component: Checkbox,
} satisfies Meta<typeof Checkbox>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Unchecked: Story = {
args: {
label: 'Unchecked',
},
};
注意此处相对 纯 JS 版 的两个差异:
import type { Meta, StoryObj } from '@storybook/your-framework'中的包名是占位符,实际使用时要替换为@storybook/react-vite、@storybook/nextjs、@storybook/vue3-vite等具体框架包(源码片段中以注释形式标注了这一点);- 用
const meta = {...} satisfies Meta<typeof Checkbox>约束元数据,再以type Story = StoryObj<typeof meta>让每个具名导出与组件的 props 类型自动对齐——写错属性名会在编译期直接报错。
Angular 版本:从组件类导入
Angular 渲染器同样遵循 CSF 3,区别在于组件来源与类型来自 @storybook/angular:
// Checkbox.stories.ts —— CSF 3(angular)
import type { Meta, StoryObj } from '@storybook/angular';
import { Checkbox } from './checkbox.component';
const meta: Meta<Checkbox> = {
component: Checkbox,
};
export default meta;
type Story = StoryObj<Checkbox>;
export const Unchecked: Story = {
args: {
label: 'Unchecked',
},
};
Angular 下 Checkbox 是从 ./checkbox.component 导入的组件类,Meta<Checkbox> / StoryObj<Checkbox> 直接以组件类型作为泛型参数,label 会被编译为组件 @Input() 的输入值。
Svelte:两种写法并存
Svelte 是这套示例中唯一提供「两套范式」的渲染器,这与仓库教程页 docs/writing-stories/index.mdx 的描述一致:可以走标准 CSF(默认导出 + 具名导出),也可以使用 Svelte 生态惯用的 @storybook/addon-svelte-csf。
标准 CSF 3(JS / TS)
// Checkbox.stories.js —— CSF 3(svelte)
import Checkbox from './Checkbox.svelte';
export default {
component: Checkbox,
};
export const Unchecked = {
args: {
label: 'Unchecked',
},
};
TypeScript 版则按常见惯例引入框架包占位符 @storybook/your-framework,实际使用时替换为 svelte-vite 或 sveltekit(该提示写在片段源码注释中):
// Checkbox.stories.ts —— CSF 3(svelte)
// Replace your-framework with svelte-vite or sveltekit
import type { Meta, StoryObj } from '@storybook/your-framework';
import Checkbox from './Checkbox.svelte';
const meta = {
component: Checkbox,
} satisfies Meta<typeof Checkbox>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Unchecked: Story = {
args: {
label: 'Unchecked',
},
};
Svelte CSF:defineMeta + <Story>
标准 CSF 对 Svelte 社区而言并非唯一选择。官方教程页将其描述为社区驱动的平行方案:用 defineMeta 描述组件,用其返回的 Story 组件定义每个 Story。JS 与 TS 两种语言下的写法几乎一致:
<!-- Checkbox.stories.svelte —— Svelte CSF(js) -->
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Checkbox from './Checkbox.svelte';
const { Story } = defineMeta({
component: Checkbox,
});
</script>
<Story
name="Unchecked"
args={{
label: 'Unchecked',
}}
/>
<!-- Checkbox.stories.svelte —— Svelte CSF(ts) -->
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Checkbox from './Checkbox.svelte';
const { Story } = defineMeta({
component: Checkbox,
});
</script>
<Story
name="Unchecked"
args={{
label: 'Unchecked',
}}
/>
Svelte CSF 的关键差异在于「命名方式」:Story 的显示名不再由导出标识符推断,而是通过 <Story> 组件的 name 属性显式声明(侧边栏直接显示 name 的值)。args、decorators、parameters 等注解同样以属性形式写在 <Story> 上(详见 docs/api/csf/index.mdx 中针对 Svelte 渲染器的说明)。还有一个易踩的坑记录在 docs/writing-stories/index.mdx 中:Svelte CSF 下不能用 args 传 children,需要在 <Story> 开闭标签之间书写子内容,它会作为 Svelte 的 children snippet prop 传入。
Web Components:直接用自定义元素标签名
Web Components 渲染器下,component 字段不再引用框架组件类/对象,而是注册后的自定义元素标签字符串(此处为 demo-checkbox),Storybook 会据此实例化对应 Custom Element:
// Checkbox.stories.js —— CSF 3(web-components)
export default {
component: 'demo-checkbox',
};
export const Unchecked = {
args: {
label: 'Unchecked',
},
};
TypeScript 版本从 @storybook/web-components-vite 导入类型。由于没有可推导 props 的组件对象,Meta 与 StoryObj 均为无泛型/宽泛形式:
// Checkbox.stories.ts —— CSF 3(web-components)
import type { Meta, StoryObj } from '@storybook/web-components-vite';
const meta: Meta = {
component: 'demo-checkbox',
};
export default meta;
type Story = StoryObj;
export const Unchecked: Story = {
args: {
label: 'Unchecked',
},
};
与 React/Vue 等版本不同,Web Components 版不需要 import 组件文件——前提是标签已在项目中注册(例如由当前文档的组件库或 preview 的全局配置完成注册)。
CSF Next(实验性 🧪):preview.meta() 与 meta.story()
代码片段中与 CSF 3 并列的选项卡 CSF Next 🧪 展示了一套仍在实验期的 API,形态上是一套「无默认导出」的函数式写法。其固定套路是:
- 从项目的
.storybook/preview(即 preview 配置文件导出)导入默认的preview; - 调用
preview.meta({ component, ... })得到meta; - 调用
meta.story({ args, ... })声明 Story,并直接具名导出。
Angular、Web Components、React、Vue 四种渲染器都给出了对应变体,结构完全平行:
// Checkbox.stories.ts —— CSF Next(angular)
import preview from '../.storybook/preview';
import { Checkbox } from './checkbox.component';
const meta = preview.meta({
component: Checkbox,
});
export const Unchecked = meta.story({
args: {
label: 'Unchecked',
},
});
// Checkbox.stories.js —— CSF Next(web-components)
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'demo-checkbox',
});
export const Unchecked = meta.story({
args: {
label: 'Unchecked',
},
});
// Checkbox.stories.ts —— CSF Next(web-components)
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'demo-checkbox',
});
export const Unchecked = meta.story({
args: {
label: 'Unchecked',
},
});
// Checkbox.stories.ts|tsx —— CSF Next(react)
import preview from '../.storybook/preview';
import { Checkbox } from './Checkbox';
const meta = preview.meta({
component: Checkbox,
});
export const Unchecked = meta.story({
args: {
label: 'Unchecked',
},
});
// Checkbox.stories.js|jsx —— CSF Next(react)
import preview from '../.storybook/preview';
import { Checkbox } from './Checkbox';
const meta = preview.meta({
component: Checkbox,
});
export const Unchecked = meta.story({
args: {
label: 'Unchecked',
},
});
// Checkbox.stories.ts —— CSF Next(vue)
import preview from '../.storybook/preview';
import Checkbox from './Checkbox.vue';
const meta = preview.meta({
component: Checkbox,
});
export const Unchecked = meta.story({
args: {
label: 'Unchecked',
},
});
// Checkbox.stories.js —— CSF Next(vue)
import preview from '../.storybook/preview';
import Checkbox from './Checkbox.vue';
const meta = preview.meta({
component: Checkbox,
});
export const Unchecked = meta.story({
args: {
label: 'Unchecked',
},
});
可见 CSF Next 的两处统一:其一,../.storybook/preview 是相对于故事文件位置的导入路径(你的项目需把该路径调整为真实相对路径);其二,Angular 从 ./checkbox.component 导组件类、React 从 ./Checkbox 具名导入、Vue 从 ./Checkbox.vue 默认导入——变化的只有组件来源,API 骨架完全一致。
片段中还保留了一段注释 <!-- JS snippets still needed while providing both CSF 3 & Next -->,说明文档站点在同时提供 CSF 3 与 CSF Next 两种选项卡时,仍需为每个语言(JS/TS)分别维护片段——这也是本代码片段存在大量平行变体的直接原因。
组件的 story 定义与 CSF 3 中 Args 的作用
无论采用上面哪种范式,Unchecked 这个 Story 的语义核心都在于 args: { label: 'Unchecked' }。按 docs/api/csf/index.mdx 的说明,Args 自 SB 6.0 起成为 Story 的标准输入:
- 可被 addons 动态更新:
Controls、Actions等 addon 可以在界面上直接修改 args,让 Story 在渲染期间实时改变,这是"文档里也能交互"的基础; - 比硬编码渲染更可移植:写法本身不依赖某个具体 addon 或框架渲染器,故事文件可以跨工具复用;
- 无需自定义 render:如果 Story 只是「把 args 展开进组件」(本例即如此),CSF 3 会使用各渲染器内置的默认渲染函数,
Unchecked里无需再写任何render。
Story 命名与显示的换算规则
若未在 Story 对象上显式提供 name,显示名按 storyNameFromExport + Lodash startCase 规则转换(docs/api/csf/index.mdx 有完整映射表)。Unchecked 这类 UpperCamelCase 单词会被原样展示;而 some_custom_NAME 这类命名则会被拆分为多词。需要特殊字符、保留字或稳定 Story ID 时,才建议改用 name 字段(Svelte CSF 的 <Story name> 即属此类)。
把 Unchecked Story 渲染进 MDX 组件文档
定义好 Checkbox.stories.* 后,接下来就是 docs/writing-docs/mdx.mdx 中 Checkbox.mdx 的编排逻辑。先通过 import * as CheckboxStories from './Checkbox.stories' 把整个故事文件的导出命名空间引入,然后用两个 Doc Block 完成两件事(完整片段见 checkbox-story.md):
<Meta of={CheckboxStories} />
# Checkbox
A checkbox is a square box that can be activated or deactivated when ticked.
Use checkboxes to select one or more options from a list of choices.
<Canvas of={CheckboxStories.Unchecked} />
<Meta of={CheckboxStories} />把文档页挂到 Checkbox 故事的相邻层级(sidebar 默认节点名为Docs,可通过name或title属性自定义)。MDX 教程页特别提醒:of应引用故事文件的完整导出集合(CheckboxStories),而不是组件本身,否则可能引发文档渲染问题;<Canvas of={CheckboxStories.Unchecked} />把Unchecked这个 Story 以内联 Canvas 形式嵌入正文——上文任何一种 CSF 变体产出的具名 Story 都能在此处被引用;- MDX 文档运行在 React 运行时中,而其中的 Story 仍按其自身渲染器(React/Vue/Angular/Svelte/Web Components 等)运行,这正是 CSF 故事文件能做到"一次定义、文档与组件生态互通"的原因。
如何选择范式
| 使用场景 | 推荐范式 | 依据 |
|---|---|---|
| React / Vue / Angular / 通用 TS 项目 | CSF 3 对象式 + satisfies |
官方推荐、类型安全、可展开复用 |
| Svelte 项目 | Svelte CSF(defineMeta/Story)或标准 CSF 3 |
docs/writing-stories/index.mdx 同时介绍两种 |
| Web Components | CSF 3,component 传自定义元素标签名 |
无需导入组件模块 |
| 尝鲜新 API、消除默认导出 | CSF Next(preview.meta()/meta.story()) |
片段中以 🧪 实验标记 |
同一份 Checkbox 故事文件之所以在 MDX 教程 中能以十余种形态呈现,正是得益于 CSF 的跨框架可移植性:掌握 Unchecked 这一个例子,也就掌握了在任何受支持渲染器中声明组件默认状态的标准套路。如需继续深入,可阅读 CSF 规范详解(含从 CSF 2 升级到 CSF 3 的 codemod 指引)、书写 Story 总览,以及 命名与层级组织 中配套的 checkbox-story-grouped 分组示例。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00