Storybook 搭配 Bootstrap 的主题切换:@storybook/addon-themes 接入与 withThemeByDataAttribute 源码解析
2026/9/7 5:48:42 网站建设 项目流程

Storybook 搭配 Bootstrap 的主题切换:@storybook/addon-themes 接入与 withThemeByDataAttribute 源码解析

【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

本文以 Storybook 仓库中 Bootstrap 主题接入指南 为核心,完整讲解如何将@storybook/addon-themes插件接入 Storybook 项目,让基于 Bootstrap 构建的 Story 支持 light/dark 颜色模式一键切换。读完本文,你将掌握该插件的安装注册流程、withThemeByDataAttribute装饰器的完整参数配置,以及插件通过 channel 事件在 Preview 与 Manager 之间同步主题状态的底层机制。

为什么需要 addon-themes

@storybook/addon-themes是 Storybook 官方的主题切换插件,用于在 Preview 中切换组件的多个主题。它的典型价值在于:设计系统往往同时存在浅色、深色甚至品牌定制模式,如果每个 Story 都要手工改代码才能预览另一种模式,评审效率会很低。该插件在 Manager 工具栏注入一个带画笔图标的切换器,点击即可切换当前 Story 的主题,并支持在单个 Story 上锁定覆盖。

从 插件的 package.json 可以看到几个关键事实:

  • 插件名称为@storybook/addon-themes,displayName 为Themes
  • 按 README 说明,要求Storybook 7.0 或更高版本
  • unsupportedFrameworks配置为react-native,即 React Native 框架下不受支持。

它提供了三种装饰器策略(withThemeByDataAttributewithThemeByClassNamewithThemeByJSXProvider,见 decorators 目录),本文聚焦 Bootstrap 官方推荐的数据属性(data attribute)策略。

第一步:安装插件

在项目中以 dev dependency 安装插件,支持三种包管理器:

# yarn yarn add -D @storybook/addon-themes
# npm npm install -D @storybook/addon-themes
# pnpm pnpm add -D @storybook/addon-themes

同时别忘了安装 Bootstrap 本体(bootstrap包)——它是 Story 样式的来源,不属于插件的依赖。

第二步:在 main 配置中注册插件

.storybook/main.jsaddons数组中加入@storybook/addon-themes

export default { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: [ '@storybook/addon-essentials', + '@storybook/addon-themes', ], };

注册后,插件会同时加载两部分运行时代码:Manager 侧的工具栏组件(src/theme-switcher.tsx)与 Preview 侧的初始 globals(src/preview.ts)。从 Preview 侧源码看,插件默认初始化了一个空的themeglobal:

// code/addons/themes/src/preview.ts export const initialGlobals: ProjectAnnotations<Renderer>['initialGlobals'] = { [KEY]: '', // KEY 即 'theme' };

这保证了即使用户没有显式设置globals.theme,装饰器读取全局状态时也不会遇到undefined

第三步:在 preview 中引入 Bootstrap 的样式与脚本

要让 Story 能用到 Bootstrap 的样式和交互脚本(如模态框、下拉菜单),需要在.storybook/preview.js中导入:

import { Preview } from '@storybook/your-renderer'; +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'bootstrap/dist/js/bootstrap.bundle'; const preview: Preview = { parameters: { /* ... */ }, }; export default preview;

bootstrap.bundle已包含 Popper.js,因此无需额外引入。这一步与插件本身无关,是 Bootstrap 项目本身的常规做法,但放在 preview 中导入能确保所有 Story 共享同一份全局样式。

第四步:用 withThemeByDataAttribute 声明主题策略

Bootstrap 原生支持 light 与 dark 两种颜色模式,也允许自定义模式,切换机制是在某个父元素上设置data-bs-theme属性。基于这个机制,只需在 preview 中挂载withThemeByDataAttribute装饰器,即可让工具栏的切换动作落到data-bs-theme上:

-import { Preview } from '@storybook/your-renderer'; +import { Preview, Renderer } from '@storybook/your-renderer'; +import { withThemeByDataAttribute } from '@storybook/addon-themes'; import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/js/bootstrap.bundle'; const preview: Preview = { parameters: { /* ... */ }, + decorators: [ + withThemeByDataAttribute<Renderer>({ + themes: { + light: 'light', + dark: 'dark', + }, + defaultTheme: 'light', + attributeName: 'data-bs-theme', + }), + ] }; export default preview;

参数详解

对照装饰器源码>const themeKey = themeOverride || selected || defaultTheme;

优先级为:story 级parameters.themes.themeOverride> 全局globals.theme(工具栏选择结果)>defaultTheme

  • 写入 DOM 属性。在useEffect中通过document.querySelector(parentSelector)找到父元素并执行setAttribute(attributeName, themes[themeKey]),依赖数组为[themeOverride, selected]——即仅当覆盖值或全局主题变化时才触发 DOM 更新,避免不必要的重渲染。

  • 对 Bootstrap 场景,效果就是:工具栏点击“dark”后,<html>上出现data-bs-theme="dark",Bootstrap 5.3 的颜色模式机制接管其余工作,所有 Story 组件随之换肤。

    工具栏切换器的呈现逻辑

    了解 theme-switcher.tsx 可以实现,工具栏组件会根据注册的主题数量自适应形态:

    • 恰好 2 个主题(如 Bootstrap 的 light/dark):渲染一个带画笔图标的切换按钮,点击直接切到另一个主题;
    • 多于 2 个主题:渲染一个Select下拉框列出全部主题;
    • 0 或 1 个主题:不渲染任何控件(hasMultipleThemes不成立时返回null)。

    此外还有两个细节值得注意:

    • 锁定态(isLocked):当当前 Story 在 meta 或 story 级设置了globals: { theme: '...' },或设置了themeOverride参数时,工具栏控件被禁用并显示 “Story override” 标签(源码),提示该 Story 的主题已被故事本身接管;
    • 整体禁用:在parameters.themes中设置disable: true可隐藏工具栏控件并关闭插件行为(参数类型定义见 types.ts)。

    在单个 Story 上覆盖主题

    虽然工具栏负责全局切换,但有时某个组件只适合固定主题预览(例如深色模式下的告警条)。可以在 meta 或 story 级用globals.theme锁定:

    export default { title: 'Example/Button', component: Button, globals: { theme: 'dark' }, // meta 级覆盖 }; export const Primary = { args: { primary: true, label: 'Button' }, }; export const PrimaryDark = { args: { primary: true, label: 'Button' }, globals: { theme: 'dark' }, // story 级覆盖 };

    这正对应装饰器取值优先级中的selected来源——pluckThemeFromContext(helpers.ts)从context.globals中读取theme键。此外 types.ts 还定义了 story 级parameters.themes.themeOverride,它的优先级高于globals.theme,适合在参数中做更细粒度的覆盖。

    完整配置速查

    将上面各步合并后,一个最小可用的 Bootstrap 主题切换配置如下:

    // .storybook/main.js export default { stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], addons: ['@storybook/addon-essentials', '@storybook/addon-themes'], };
    // .storybook/preview.js import { Preview, Renderer } from '@storybook/your-renderer'; import { withThemeByDataAttribute } from '@storybook/addon-themes'; import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap/dist/js/bootstrap.bundle'; const preview = { parameters: { /* ... */ }, decorators: [ withThemeByDataAttribute<Renderer>({ themes: { light: 'light', dark: 'dark' }, defaultTheme: 'light', attributeName: 'data-bs-theme', }), ], }; export default preview;

    最后需要提醒三个适用前提与限制:一是插件要求 Storybook 7.0+ 且不支持 React Native 框架;二是withThemeByDataAttribute依赖浏览器 DOM(内部使用document.querySelector与 React 的useEffect),因此仅适用于基于 DOM 的渲染环境;三是它只对parentSelector选中的元素生效,若你的组件渲染在独立 Shadow DOM 或 iframe 内,属性不会自动穿透,需要自定义策略(可参考 插件 API 文档 中“编写自定义装饰器”的思路)。

    如果你想换用其他主题方案,同一目录下还有 emotion、styled-components、material-ui、tailwind 等针对各自工具链的接入指南,其核心装饰器用法与本文一致,只是落地的属性或 Provider 不同。

    【免费下载链接】storybookStorybook is the industry standard workshop for building, documenting, and testing UI components in isolation项目地址: https://gitcode.com/GitHub_Trending/st/storybook

    创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

    需要专业的网站建设服务?

    联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

    立即咨询