首页
/ Storybook A11y Addon 精准控场:用 `globals.a11y.manual` 按 Story 关闭自动无障碍检查

Storybook A11y Addon 精准控场:用 `globals.a11y.manual` 按 Story 关闭自动无障碍检查

2026-09-06 18:40:27作者:尤峻淳Whitney

导读

在 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/previewglobals 里。

四、源码级原理: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.manualparameters.a11y 是两条独立的开关通道:前者走 globals(可在运行时切换),后者走 parameters(声明式静态配置),二者是"与"关系——任何一条把开关置为"关",自动扫描都不会发生。

3. 手动通道与面板状态

当 Story 处于 manual 模式,扫描并非被彻底禁用,而是被移交到事件驱动的手动通道。在 code/addons/a11y/src/a11yRunner.ts 中,addon 监听 EVENTS.MANUAL 事件,收到后先 waitForAnimations() 等待动画稳定,再执行 run(input, storyId) 并把结果回传到面板。

面板侧,A11YPanel.tsxstatus === '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 trueparameters.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" 并要求你改回 disabletest(见 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"。这说明精确的规则与粒度控制(而非一刀切关闭)才是团队的主流姿势。

七、实践清单

把本文内容落地为可执行动作:

  1. 先评估归属:要关的是"某个反模式 Story"还是"整份文件的全部 Story"?前者写在该 Story 的 globals 里,后者提升到 metaglobals
  2. 按框架选语法:CSF 3 用 satisfies + StoryObj(React/Vue/Web Components)或泛型 Meta<T>/StoryObj<T>(Angular);CSF Next 用 preview.meta(...).story(...);Svelte 用户优先考虑 Svelte CSF 的 <Story globals={...}>
  3. 保留手动通道:配置只关闭"自动",请善用 Accessibility 面板的 "Run accessibility scan" 做人工复核,避免把违规静默吞掉。
  4. 不要把它当逃逸出口manual/'off' 面向的是"无需自动化"的场景;对"已知有违规但待修复"的 Story,官方推荐用 parameters.a11y.test: 'todo' 保留可见性,配合 Recommended workflow 渐进清零。

理解了 globals.a11y.manual 的执行边界,你就能在 Storybook 的无障碍体系中精确区分"应该自动守护的部分"与"交给人工判断的部分",让 a11y 检查既全面又不制造噪音。

登录后查看全文
热门项目推荐
相关项目推荐