Storybook A11y Addon 精准控场:用 `globals.a11y.manual` 按 Story 关闭自动无障碍检查
导读
在 Storybook 中做无障碍(Accessibility)测试时,并非每个 Story 都适合被自动扫描——例如用于演示反模式(antipattern)或特定异常状态的组件,自动化检查反而会制造大量干扰性告警。本文基于 Storybook 官方 Accessibility 测试文档中的代码片段,系统讲解如何通过 globals.a11y.manual: true 在单个 Story(乃至 meta / 组件)层面关闭自动 a11y 检查,覆盖 React、Angular、Vue、Svelte、Web Components 的 CSF 3 与 CSF Next 写法,并结合 addon-a11y 的源码剖析其底层执行逻辑与三种禁用途径的取舍。读完你将能精确控制"何时自动扫描、何时仅手动扫描",让无障碍流水线既严格又不误伤。
一、先理解:为什么需要按 Story 关闭自动检查
Storybook 的 Accessibility addon 以 axe-core 为引擎,在你访问某个 Story 或通过 Vitest addon、test-runner 运行测试时,会对渲染出的 DOM 自动执行无障碍规则检查。然而自动扫描并非在所有场景下都是期望行为,官方文档明确指出两个典型动机:
- 反模式演示:某些 Story 的存在意义就是"展示一个有问题的写法",例如刻意缺失 label 的输入框,用于文档教学或视觉对比。此时自动扫描必然报违规,属于误报噪音。
- 特定展示用途:Story 本身不以无障碍达标为目标(如只演示布局、动效),测试它没有意义。
在这一前提下,Storybook 提供的是**"不是一次测不测,而是测不测得出"**的能力——你仍然可以随时在 Accessibility 面板里手动触发扫描。关闭的只是"自动"那一环,而非整个无障碍能力。
本文对应的完整文档章节位于 docs/writing-tests/accessibility-testing.mdx,其中以 <CodeSnippets path="addon-a11y-disable.md" /> 引入的代码即 docs/_snippets/addon-a11y-disable.md 这一片段集合,覆盖了全部主流 renderer 的等价写法。
二、核心配置:globals.a11y.manual
关闭某个 Story 的自动无障碍检查,做法是在该 Story 上声明一段 globals:
export const NonA11yStory: Story = {
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
};
其语义在代码注释与官方文档中一致:manual: true 会让这个 Story 不被自动分析。关闭后:
- 你导航到这个 Story 时,addon 不会自动跑 axe 扫描;
- 用 Vitest addon / test-runner 跑测试时,也不会把它计入自动 a11y 用例;
- 你依然可以在 Accessibility 面板中点 "Run accessibility scan" 按钮手动执行检查。
用一句话记忆:manual 的含义是"这个 Story 的扫描交给人工,不做自动化"。
globals 与 Story 生命周期
globals 是 Storybook 的全局状态机制(在 parameters 文档 对应的核心概念中属于运行时可变状态),因此 a11y.manual 不仅在 Story 定义阶段生效,而且支持在 addon 面板里运行时切换——A11YPanel 源码中会提示 "Update globals.a11y.manual to disable manual mode"(见 code/addons/a11y/src/components/A11YPanel.tsx),说明这是一个可交互的活配置。
三、主流框架与多种 CSF 写法的完整示例
以下代码等价地实现同一目标:为名为 NonA11yStory(或 ExampleStory)的 Story 关闭自动检查。核心都是 globals.a11y.manual: true,差异仅在于各 renderer / CSF 语法形态。
React(CSF 3)
带 TypeScript 类型收窄的标准写法使用 satisfies 保证类型安全:
// MyComponent.stories.ts|tsx
// Replace your-framework with the framework you are using, e.g. react-vite, nextjs, nextjs-vite, etc.
import type { Meta, StoryObj } from '@storybook/your-framework';
import { MyComponent } from './MyComponent';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const NonA11yStory: Story = {
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
};
纯 JavaScript 版本(.js|jsx)去掉了类型标注,结构与上面完全一致:
// MyComponent.stories.js|jsx
import { MyComponent } from './MyComponent';
export default {
component: MyComponent,
};
export const NonA11yStory = {
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
};
React(CSF Next 🧪)
在实验性的 CSF Next(通过 preview.meta / preview.story 构造)中,写法变为链式 API,配置点落在 meta.story({...}) 内:
// MyComponent.stories.ts|tsx
import preview from '../.storybook/preview';
import { MyComponent } from './MyComponent';
const meta = preview.meta({
component: MyComponent,
});
export const NonA11yStory = meta.story({
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
});
Angular
Angular 的 CSF 3 使用来自 @storybook/angular 的类型:
// MyComponent.stories.ts
import type { Meta, StoryObj } from '@storybook/angular';
import { MyComponent } from './my-component.component';
const meta: Meta<MyComponent> = {
component: MyComponent,
};
export default meta;
type Story = StoryObj<MyComponent>;
export const NonA11yStory: Story = {
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
};
CSF Next 版本同样通过 preview 组织:
// MyComponent.stories.ts
import preview from '../.storybook/preview';
import { MyComponent } from './my-component.component';
const meta = preview.meta({
component: MyComponent,
});
export const NonA11yStory = meta.story({
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
});
Vue 3
CSF 3(以 vue3-vite 为例):
// MyComponent.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3-vite';
import MyComponent from './MyComponent.vue';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const NonA11yStory: Story = {
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
};
CSF Next:
// MyComponent.stories.ts
import preview from '../.storybook/preview';
import MyComponent from './MyComponent.vue';
const meta = preview.meta({
component: MyComponent,
});
export const NonA11yStory = meta.story({
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
});
纯 JS 的 Vue CSF 3 版本同样只需去掉类型注解,结构不变(参见 addon-a11y-disable.md 中的 MyComponent.stories.js 片段)。
Svelte
Svelte 用户有两个入口:普通 CSF 3,以及 Svelte CSF(来自 @storybook/addon-svelte-csf)。
Svelte CSF 通过 <Story> 组件的 globals 属性传入,此时 defineMeta 负责声明组件元信息:
<!-- MyComponent.stories.svelte -->
<script module>
import { defineMeta } from '@storybook/addon-svelte-csf';
import MyComponent from './MyComponent.svelte';
const { Story } = defineMeta({
component: MyComponent,
});
</script>
<Story
name="NonA11yStory"
globals={{
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
}}
/>
Svelte 的普通 CSF 3 写法与其他框架一致:
// MyComponent.stories.ts
// Replace your-framework with the framework you are using, e.g. sveltekit or svelte-vite
import type { Meta, StoryObj } from '@storybook/your-framework';
import MyComponent from './MyComponent.svelte';
const meta = {
component: MyComponent,
} satisfies Meta<typeof MyComponent>;
export default meta;
type Story = StoryObj<typeof meta>;
export const NonA11yStory: Story = {
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
};
Web Components
Web Components 通过自定义元素名声明组件(JS 与 TS 版本同理):
// MyComponent.stories.ts
import type { Meta, StoryObj } from '@storybook/web-components-vite';
const meta: Meta = {
component: 'my-component',
};
export default meta;
type Story = StoryObj;
export const ExampleStory: Story = {
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
};
CSF Next 版本:
// MyComponent.stories.ts
import preview from '../.storybook/preview';
const meta = preview.meta({
component: 'my-component',
});
export const ExampleStory = meta.story({
globals: {
a11y: {
// This option disables all automatic a11y checks on this story
manual: true,
},
},
});
提示:上面各示例均写在单个 Story 上。文档同时说明,同样的 globals 可以放到
meta(default export)中,从而对整份文件内所有 Story 生效;如果你需要的是项目级全局生效,则把它提升到 .storybook/preview 的globals里。
四、源码级原理:manual: true 之后发生了什么
仅仅会写配置还不够,理解 addon 的执行路径有助于判断配置何时生效。阅读 addon-a11y 的 preview 侧源码可以还原完整链路。
1. 默认值:manual 初始为 false
在 code/addons/a11y/src/preview.tsx 中,addon 声明了初始全局状态:
export const initialGlobals = {
a11y: {
manual: false,
},
vision: undefined,
};
也就是说,默认行为是"自动扫描",只有显式声明 manual: true 的 Story 才会被豁免。
2. afterEach 钩子里的三重开关
addon 通过 afterEach 生命周期钩子执行自动检查,其判定逻辑是(见 preview.tsx):
const shouldRunEnvironmentIndependent =
!isGhostStories &&
a11yParameter?.disable !== true &&
a11yParameter?.test !== 'off' &&
a11yGlobals?.manual !== true;
当且仅当上述四个条件全部满足(非幽灵运行、未显式 disable、test 非 'off'、manual 非 true),才会调用 run(a11yParameter, storyId) 执行 axe 扫描,并把结果以报告形式写入 reporting.addReport。由此可以看出,globals.a11y.manual 与 parameters.a11y 是两条独立的开关通道:前者走 globals(可在运行时切换),后者走 parameters(声明式静态配置),二者是"与"关系——任何一条把开关置为"关",自动扫描都不会发生。
3. 手动通道与面板状态
当 Story 处于 manual 模式,扫描并非被彻底禁用,而是被移交到事件驱动的手动通道。在 code/addons/a11y/src/a11yRunner.ts 中,addon 监听 EVENTS.MANUAL 事件,收到后先 waitForAnimations() 等待动画稳定,再执行 run(input, storyId) 并把结果回传到面板。
面板侧,A11YPanel.tsx 在 status === 'manual' 时显示:
Accessibility tests run manually for this story — Results will not show when using the testing module. You can still run accessibility tests manually.
并渲染一个 "Run accessibility scan" 按钮,供你随时手动扫描该 Story。这正是官方文档所述"关闭自动检查后仍可在面板手动验证"的实现基础。
4. 测试用例印证三种关闭途径
addon 的单元测试 code/addons/a11y/src/preview.test.tsx 为三条关闭途径各写了独立用例,可作为回归验证的锚点:
should not run accessibility checks when disable is true(parameters.a11y.disable);should not run accessibility checks when globals manual is true(即本文主题,其断言处把globals设为manual: true);should not run accessibility checks when parameters.a11y.test is "off"。
若你想验证自己的项目行为,可在测试运行中加入对上述语义的断言。
五、三种"不自动测"的途径怎么选
结合上文源码可知,addon 实际提供三条关闭自动检查的路径,容易混淆,此处给出选择建议:
| 途径 | 写法位置 | 语义 | 适用场景 |
|---|---|---|---|
globals.a11y.manual: true |
Story / meta / preview 的 globals |
该 Story 仅支持手动扫描,自动环节(导航访问、测试运行)跳过 | 反模式演示、无需自动达标的展示型 Story |
parameters.a11y.disable: true |
Story / meta / preview 的 parameters |
整体关闭该 Story 的无障碍测试,面板显示 "Accessibility tests are disabled for this story" | 明确表示该 Story 不参与无障碍体系(类型见 code/addons/a11y/src/params.ts) |
parameters.a11y.test: 'off' |
Story / meta / preview 的 parameters |
不运行自动化 a11y 测试,但仍可在面板手动验证 | 已知暂不修复、或无需测试的组件 |
三者间最显著的差别在于面板提示与测试语义:manual 状态下面板明确告知"本 Story 测试改为手动运行",仍提供扫描按钮;parameters.a11y.disable 时面板直接显示 "Accessibility tests are disabled for this story" 并要求你改回 disable 或 test(见 A11YPanel.tsx)。
六、与自动化测试链的关系:CI 里到底还跑不跑
如果你在跑组件测试,需要理解 manual: true 在 CI / 测试链中的后果:
- 使用 Vitest addon(集成指南)时,被标记为
manual的 Story 不会进入自动 a11y 用例集——因为它连axe.run都不会被调用。 - 使用 test-runner 同理:文档说明无障碍测试只有在 addon 已安装且
parameters.a11y.test非'off'时才包含进测试运行,manual: true使该 Story 从该集合中剔除。 - 反向提醒:
parameters.a11y.test: 'off'只应服务于"确实不需要测试"的 Story(例如演示反模式)。如果你希望自动化测试保持严格,请优先使用'todo'标记暂存问题,而不是用'off'逃避,详见 accessibility-testing.mdx 的 Test behavior 章节。
值得注意的是,axe 在组件测试场景可能产生误报,例如默认情况下 addon 会关闭 region 规则 这类针对地标(landmark)的检查——在 code/addons/a11y/src/a11yRunner.ts 中通过 DISABLED_RULES 常量体现,原因是 "In component testing, landmarks are not always present"。这说明精确的规则与粒度控制(而非一刀切关闭)才是团队的主流姿势。
七、实践清单
把本文内容落地为可执行动作:
- 先评估归属:要关的是"某个反模式 Story"还是"整份文件的全部 Story"?前者写在该 Story 的
globals里,后者提升到meta的globals。 - 按框架选语法:CSF 3 用
satisfies+StoryObj(React/Vue/Web Components)或泛型Meta<T>/StoryObj<T>(Angular);CSF Next 用preview.meta(...).story(...);Svelte 用户优先考虑 Svelte CSF 的<Story globals={...}>。 - 保留手动通道:配置只关闭"自动",请善用 Accessibility 面板的 "Run accessibility scan" 做人工复核,避免把违规静默吞掉。
- 不要把它当逃逸出口:
manual/'off'面向的是"无需自动化"的场景;对"已知有违规但待修复"的 Story,官方推荐用parameters.a11y.test: 'todo'保留可见性,配合 Recommended workflow 渐进清零。
理解了 globals.a11y.manual 的执行边界,你就能在 Storybook 的无障碍体系中精确区分"应该自动守护的部分"与"交给人工判断的部分",让 a11y 检查既全面又不制造噪音。
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 StartedRust0624
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