为 Storybook 组件参数声明候选值:argTypes.options 完整使用指南
在 Storybook 中,当组件的某个参数只接受一组有限取值(例如图标名 arrow-up/arrow-down/loading、按钮样式 primary/secondary、枚举色板等)时,你可以在 argTypes 上通过 options 属性把这段候选值集合告诉 Storybook。声明之后,Controls 面板会据此自动渲染成下拉、单选按钮或复选框等更友好的控件,Storybook 运行时会拦截并提示传入的非法值,配合 mapping 与 control.labels 还能把字符串候选映射为组件真正需要的复杂对象或带有人类可读的展示标签。
本文以 arg-types-options 代码片段 与 argTypes API 文档 为骨架,结合仓库中 Controls 的实现源码,讲解 options 的语义、默认值推断规则、声明位置、跨框架(Angular/Svelte/React/Vue/Web Components)写法,以及它背后「控件类型推断 + 运行时值校验」的完整链路。
一、先理解 options 的定位:它属于 argTypes
在 docs/api/arg-types.mdx 中,Storybook 把 argTypes 描述为“指定 args 行为”的对象:通过声明参数的类型,你可以约束该参数可接受的取值,并为没有被显式赋值的参数提供描述信息。argTypes 的整体结构如下(节选):
{
[key: string]: {
control?: ControlType | { type: ControlType; /* ... */ } | false;
description?: string;
if?: Conditional;
mapping?: { [key: string]: { [option: string]: any } };
name?: string;
options?: string[]; // 本文主角
table?: { /* category / defaultValue / disable / subcategory / type */ },
type?: SBType | SBScalarType['name'];
}
}
其中每个 argType 的 options 属性:
- 类型:
string[] - 默认值:由自动推断产生
它的用途在文档中写得很明确:如果某个 arg 只接受一组有限的取值,就用 options 把它们列出来。这也正是组件开发中最常见的一类需求——图标库的名称集合、主题变体、尺寸规格、国家列表、状态码等。
二、自动推断的来源
默认情况下,只要你在故事文件的 meta(default export)里通过 component 注解指向真实组件,Storybook Docs 就会根据组件定义自动推断一组 argTypes。为此,不同框架会使用不同的静态分析工具:
| 框架 | 静态分析工具(依据 arg-types.mdx) |
|---|---|
| React | react-docgen(默认)或 react-docgen-typescript |
| Vue | vue-docgen-api |
| Angular (Vite) | 读取 TypeScript 源码(Storybook server 端),或改用 Compodoc |
| Angular (Webpack) | Compodoc |
| Web Components | custom-element.json |
| Ember | YUI doc |
这些工具的产出与 argTypes 数据结构刻意保持一致。手动声明的属性会覆盖推断值。对于 options 而言,最常见的自动推断路径是:当文档工具把组件的某个 prop 识别为枚举类型时,它会产生类似 type: { name: 'enum', value: [...] } 的结构,随后 Controls 的推断器会把这组枚举值翻译成可供交互的候选列表(详见下文“options 如何被使用”)。
推断 argTypes 需要 docs 环境
需要留意的是自动推断与 Storybook Docs 插件 是否启用有关。若不依赖组件推断,而你的框架又不支持组件元数据读取(HTML、部分自定义框架等),controls 文档 会明确建议为组件手动声明 argTypes——这正是 options 这一节代码片段存在的核心场景之一。
三、在哪个作用域声明 options
argTypes(连同其中的 options)可以在三个层级声明,遵循“越具体越优先”的覆盖规则:
- 组件级(meta / default export):对组件所有故事生效,也是参数候选值声明的默认位置。下面所有示例都属于这种写法。
- 项目级(全局):写在
.storybook/preview.*配置文件中,作用于所有故事。 - 故事级:仅对单个故事生效,见 arg-types-in-story 片段。
由于 options 描述的是组件属性的取值边界,实践中绝大多数写在组件级 meta 中;全局声明适合为跨组件共享的同名参数(如统一的 size/variant 约定)提供兜底。
组件级声明的最小骨架(通用 CSF 3 写法)如下,对应 arg-types-in-meta 片段:
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta } from '@storybook/your-framework';
import { Example } from './Example';
const meta = {
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
} satisfies Meta<typeof Example>;
export default meta;
四、核心示例:为 icon 声明候选图标值(跨框架完整写法)
下面完整继承 arg-types-options.md 中提供的全部写法。示例的业务背景是:Example 组件的 icon 参数只接受 'arrow-up'、'arrow-down'、'loading' 三个图标名,我们把它声明到组件级 meta 的 argTypes 中。
Angular(TypeScript)
CSF 3:
import type { Meta } from '@storybook/angular';
import { Example } from './example.component';
const meta: Meta<Example> = {
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
};
export default meta;
CSF Next(实验性):
import preview from '../.storybook/preview';
import { Example } from './example.component';
const meta = preview.meta({
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
});
Svelte
@storybook/addon-svelte-csf 的 defineMeta(Svelte CSF),支持 .stories.svelte:
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import Example from './Example.svelte';
const { Story } = defineMeta({
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
});
</script>
Svelte 的普通 CSF 3(JavaScript):
import Example from './Example.svelte';
export default {
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
};
Svelte 的普通 CSF 3(TypeScript,需把 your-framework 替换为 svelte-vite 或 sveltekit):
// Replace your-framework with svelte-vite or sveltekit
import type { Meta } from '@storybook/your-framework';
import Example from './Example.svelte';
const meta = {
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
} satisfies Meta<typeof Example>;
export default meta;
React 与通用框架(CSF 3)
JavaScript(.js|jsx):
import { Example } from './Example';
export default {
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
};
TypeScript(.ts|tsx),需把 your-framework 替换为实际使用的框架包(如 react-vite、nextjs、vue3-vite 等):
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, vue3-vite, etc.
import type { Meta } from '@storybook/your-framework';
import { Example } from './Example';
const meta = {
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
} satisfies Meta<typeof Example>;
export default meta;
React 的 CSF Next(实验性,从项目级 preview 导入元数据):
import preview from '../.storybook/preview';
import { Example } from './Example';
const meta = preview.meta({
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
});
提示:
<CodeSnippets>会为同一逻辑同时渲染 CSF 3 与 CSF Next 两套 JS 片段,以保证在过渡期内两类格式都有可直接复制的版本;仓库源码注释也注明了这一策略。
Vue(CSF Next)
import preview from '../.storybook/preview';
import Example from './Example.vue';
const meta = preview.meta({
component: Example,
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
});
Web Components
CSF 3(JavaScript),component 使用自定义元素标签名 'demo-example':
export default {
component: 'demo-example',
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
};
CSF 3(TypeScript):
import type { Meta } from '@storybook/web-components-vite';
const meta: Meta = {
component: 'demo-example',
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
};
export default meta;
CSF Next(JavaScript / TypeScript):
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'demo-example',
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
});
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'demo-example',
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
},
},
});
五、options 与控件类型的搭配:什么时候必须提供
options 本身不会凭空产生 UI,它真正起作用的地方是和 control(及其 control.type)联动。在 arg-types.mdx 的 Controls 矩阵中,凡是“枚举”型数据的控件都需要 options 提供可选项:
| 数据类型 | control.type | 说明 |
|---|---|---|
| enum | 'check' |
一组堆叠的复选框,可多选 |
| enum | 'inline-check' |
一组内联复选框,可多选 |
| enum | 'radio' |
一组堆叠的单选按钮 |
| enum | 'inline-radio' |
一组内联单选按钮 |
| enum | 'select' |
单个下拉选择 |
| enum | 'multi-select' |
支持多选的下拉列表 |
示例如下(来自 arg-types.mdx 的说明性配置):
argTypes: {
contact: { control: 'radio', options: ['email', 'phone', 'mail'] },
age: { control: 'select', options: [20, 30, 40, 50] },
country: { control: 'multi-select', options: ['USA', 'Canada', 'Mexico'] },
}
control 的默认推断规则
当你在 argType 中只写 options、不写 control(即本文核心示例的写法)时,arg-types.mdx 规定了如下默认值查找顺序:
- 若指定了
options→ 控件默认为'select'; - 否则根据
type推断; - 再否则回退为
'object'。
也就是说,把 icon: { options: [...] } 丢给 Storybook,Controls 面板会自动渲染成一个 select 下拉框,让使用者从候选中选取,而不是手敲文本。这也是该写法在官方文档中的典型预期效果:比自由文本输入更不易出错。
control.type 单独指定时的默认逻辑
若显式给出 control: { type: ... },options 是 radio/inline-radio/select/multi-select/check/inline-check 等枚举类控件的必备数据源,源码层面这些控件的类型联合中专门带上了 labels 字段用于展示定制(见 code/core/src/csf/story.ts)。
六、源码视角:options 究竟如何被使用
要真正理解 options 的威力,需要看 preview(运行时)侧的两条处理链路。它们都集中在 code/core/src/preview-api/modules/store 目录。
链路一:控件推断(argTypesEnhancer)
在 inferControls.ts 中,Controls 会遍历每个 argType 的 type 与 options 生成最终控件配置:
switch (type.name) {
case 'array':
return { control: { type: 'object' } };
case 'boolean':
return { control: { type: 'boolean' } };
case 'string':
return { control: { type: 'text' } };
case 'number':
return { control: { type: 'number' } };
case 'enum': {
const { value } = type as SBEnumType;
return { control: { type: value?.length <= 5 ? 'radio' : 'select' }, options: value };
}
case 'function':
case 'symbol':
return null;
default:
return { control: { type: options ? 'select' : 'object' } };
}
从这段源码可以确认两件事:
- 自动推断的枚举:当静态分析工具把组件属性识别为
enum(比如value?.length即枚举成员数)时,Storybook 会从type.value提取枚举值填进options,并且成员数 ≤ 5 时默认渲染radio,超过 5 时渲染select。 - 只写
options无类型:落入default分支时,options是否存在直接决定控件是select还是object——这与 arg-types.mdx 中“指定 options 则默认 select”的文档规则完全对应。
换句话说:手动写 icon: { options: [...] } 实际上是在为默认控件推断提供 options 数据源并锁定 select 控件。
链路二:args 清洗与非法值拦截
options 不仅是 UI 的数据源,它还参与 args 的运行时校验。在 args.ts 的清洗逻辑里,Storybook 会对每个带 options 的 arg 做以下检查:
if (!Array.isArray(options)) {
once.error('Invalid argType: ... options should be an array. ...');
}
if (options.some((opt) => opt && ['object', 'function'].includes(typeof opt))) {
once.error('Invalid argType: ... options should only contain primitives. Use a "mapping" for complex values. ...');
}
// 数组中每个值都必须命中候选集合
if (args[key] === undefined || options.includes(args[key]) || isValidArray) {
return allowArg();
}
// 否则跳过该 arg 并输出警告
once.warn(`Received illegal value for ... Supported options: ...`);
据此可以提炼出三条硬性约束与行为:
options必须是数组,否则控制台会报Invalid argType错误;options只能存放原始值(primitive)。对象、函数等复杂值会被拒绝,源码错误信息会直接提示你改用mapping(见下文第七节);- 当外部传入(例如通过 URL 深链恢复、或其他渠道设置)的 args 值不在候选集合内时,Storybook 会丢弃该值并打印警告,列出支持的候选值——这从机制上保证了组件永远收不到约定之外的取值。
七、用 options + mapping/labels 表达复杂取值与可读标签
为什么复杂值需要 mapping
如果组件真正需要的是复杂值(例如 JSX 元素、带样式的对象),直接塞进 options 是不行的。除了上节源码中“只允许原始值”的硬约束之外,arg-types.mdx 还从产品层面给出解释:并非所有值都能被序列化进 URL 的 args 参数,这意味着一旦 arg 携带复杂值,就无法通过 URL 分享/深链该状态;同时 JSX 等复杂值无法在 manager(如 Controls 面板)与 preview(你的故事)之间同步。
标准解法是用字符串做 options 的候选,再用 mapping 把它们映射为渲染前真正使用的复杂值。形如(示意):
argTypes: {
icon: {
options: ['arrow-up', 'arrow-down', 'loading'],
mapping: {
'arrow-up': <Icon name="arrow-up" />, // 原始候选 → 复杂 JSX 值
},
},
}
对应文档另见 arg-types-mapping 片段。两个附加规则值得记住:
mapping不必穷举;未命中的选项会原样透传(verbatim);mapping与 URL 查询串中使用的是原始字符串候选,因此分享的链接仍然稳定。
用 control.labels 给候选换显示名
如果候选值本身不适合直接展示(例如代码风格的值 'us' 想显示成 United States),可以配合 control.labels:
argTypes: {
country: {
options: ['USA', 'Canada', 'Mexico'],
control: {
type: 'select',
labels: { USA: 'United States', Canada: 'Canada', Mexico: 'Mexico' },
},
},
}
labels 同样不必穷举,未命中的选项会按原值显示。它在底层被限定在 inline-check | radio | inline-radio | select | multi-select 这些枚举类控件上使用(见 code/core/src/csf/story.ts 的类型定义)。
八、在 Storybook 类型与 UI 层的落实
options 的顶层类型定义位于 CSF 的 StrictInputType 中(code/core/src/csf/story.ts):
/** @see https://storybook.js.org/docs/api/arg-types#options */
options?: readonly any[];
这里需要注意文档(写作 string[])与运行时类型(readonly any[])之间的细微差异:文档以字符串作为最典型场景进行说明,而底层放开了成员类型的约束,number(如下拉年龄选项 [20, 30, 40, 50])同样常见,只是源码要求在运行时必须是“原始值数组”。
至于 UI 层,controls 模块 中承载的是面向使用者的面板级 parameters.controls 配置(disable、exclude/include、expanded、sort、matchers、presetColors 等)。options 本身属于 argType 注解、由 Controls 面板逐行读取渲染;当同时开启 expanded 参数时,Controls 面板还会以内嵌 Controls doc block 的形式展示每个属性的完整说明与默认值。argTypes 的最终可视化形态,则对应 ArgTypes doc block(以及功能类似的 Controls doc block)——表格中的每一行就是一个 argType 与它当前的值。
九、实践建议与易错点小结
结合文档与源码,使用 argTypes.options 时建议遵循以下规则:
- 候选集合不大、语义清晰的参数优先声明:图标、变体、尺寸、枚举状态等;它同时改进 Controls 交互与运行期数据安全性。
- 能用推断就别手写:如果你的框架支持组件静态分析,且组件 prop 本身是枚举/字面量联合类型,Storybook 会自动推导;手动声明会在类型推断基础上做覆盖,请注意与自动推断的结果保持一致,避免两套取值集合漂移。
- 保持原始值 + mapping 的组合习惯:需要复杂值就上
mapping,不要试图在options里放对象/函数,否则 preview 会在运行时直接报错。 - 展示名称交给
labels,而不是改options:把“候选值”与“展示文案”解耦,才能同时保证 URL 深链稳定与 UI 可读。 - 理解“只写 options”的默认控件:不指定
control时默认得到select;想要单选按钮需显式声明control: { type: 'radio' }(自动推断的枚举场景下 ≤5 个成员会自动用 radio)。 - 作用域最小化:组件级声明为主;若要定义跨项目统一的命名约定再考虑放进 全局 preview 配置;单个故事的特殊取值边界用故事级 argTypes 覆盖。
最终,你可以在 docs/_snippets/arg-types-options.md 与 docs/api/arg-types.mdx 查看 options 的完整参考,在 docs/essentials/controls.mdx 中查看 Controls 面板整体配置与“处理复杂值”的更多讨论,并深入 code/core/src/preview-api/modules/store/inferControls.ts、args.ts 阅读其底层实现。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00