首页
/ Storybook addon-themes 实战:为 Tailwind CSS 配置一键切换 light/dark 主题

Storybook addon-themes 实战:为 Tailwind CSS 配置一键切换 light/dark 主题

2026-09-06 13:46:06作者:羿妍玫Ivan

在基于 Tailwind CSS 的组件库项目中,开发者经常需要在预览环境里反复确认组件在亮色(light)与暗色(dark)模式下的表现。Storybook 官方的 @storybook/addon-themes 插件正是解决这一痛点的工具:它会在 Storybook 工具栏中注入一个 "Themes" 下拉控件,让你用一次点击就能在所有已声明的主题之间切换。本文以仓库中的 Tailwind 入门指南 为主体,完整覆盖安装、注册、CSS 引入、withThemeByClassNamewithThemeByDataAttribute 两类装饰器的配置方法,并结合插件源码剖析其"装饰器 → 全局状态 → 工具栏"的联动原理。

前置说明:插件定位与版本前提

@storybook/addon-themes 的定位是"从工具栏在多个主题之间切换",当前仓库中该插件的 package.json 显示其版本为 10.6.0-beta.1,并且要求 peer 依赖 storybook 为同仓库工作区版本;根据 插件 README,该插件要求 Storybook 7.0 或更高版本。README 同时说明插件支持的工具链包括 emotion、MUI、Bootstrap、PostCSS、styled-components、Tailwind 与 Vuetify 3.x,本文聚焦其中的 Tailwind 方案。

此外,从 package.jsonstorybook.unsupportedFrameworks 字段可以确认:该插件明确不支持 react-native 框架,其余 React、Vue、Angular、Svelte、Web Components 等渲染器均可使用。

安装插件

@storybook/addon-themes 作为 dev 依赖安装到你的 Storybook 所在项目中,三种常用包管理器均可:

# yarn
yarn add -D @storybook/addon-themes

# npm
npm install -D @storybook/addon-themes

# pnpm
pnpm add -D @storybook/addon-themes

在 main 配置中注册 Addon

安装完成后,需要把插件加入 .storybook/main.js(或 main.ts)的 addons 数组,Storybook 才会加载它的 manager 与 preview 两侧代码:

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

从源码结构看,注册字符串对应的是 src/index.ts 导出的 definePreviewAddon 入口——它把 preview 侧的默认 annotations(见下文)挂进预览项目;而 manager 侧则由独立的入口(manager.js 指向 src/manager.tsx)完成工具栏控件的注册。

在 preview 中引入 Tailwind CSS

要让 story 真正应用 Tailwind 样式,需要把项目里的样式入口(通常是包含 @tailwind 指令的 src/index.css)导入 .storybook/preview.js

import { Preview } from '@storybook/your-renderer';

+import '../src/index.css';

const preview: Preview = {
  parameters: { /* ... */ },
};

export default preview;

这一点是 Tailwind 方案与 styled-components 等"JS 层主题"方案的本质区别:Tailwind 的暗色样式依赖编译后 CSS 中的类名选择器(.dark 变体),样式必须先注入预览环境的文档,后文的装饰器切换才能产生可见效果。

方案一:使用类名切换主题(withThemeByClassName

Tailwind 原生支持通过给父元素(通常是 <html>)添加 .dark 类来激活暗色模式。为此,把 withThemeByClassName 装饰器加入 .storybook/preview.js

-import { Preview } from '@storybook/your-renderer';
+import { Preview, Renderer } from '@storybook/your-renderer';
+import { withThemeByClassName } from '@storybook/addon-themes';

import '../src/index.css';

const preview: Preview = {
  parameters: { /* ... */ },
+ decorators: [
+  withThemeByClassName<Renderer>({
+    themes: {
+      light: '',
+      dark: 'dark',
+    },
+    defaultTheme: 'light',
+  }),
+ ]
};

export default preview;

配置项含义(结合 class-name.decorator.tsxClassNameStrategyConfiguration 接口):

配置项 类型 说明
themes Record<string, string> 主题名到类名串的映射。light: '' 表示亮色模式不添加任何类,dark: 'dark' 表示暗色模式给父元素添加 dark
defaultTheme string 未选择主题时默认应用的主题名,工具栏初始值也取自这里
parentSelector string(可选) 目标元素的 CSS 选择器,默认为 'html'(源码常量 DEFAULT_ELEMENT_SELECTOR

装饰器的运行时行为(源码可验证):

  1. 初始化时通过 initializeThemeState 向 addons 通道发出 REGISTER_THEMES 事件,上报主题名列表与默认主题,工具栏据此渲染下拉选项(见 helpers.tsconstants.ts 中的 THEMING_EVENTS.REGISTER_THEMES);
  2. 每次 story 渲染时,在 useEffect 中先移除除当前选中主题外所有主题对应的类(类名串会按空格拆分为数组后整体 classList.remove,因此一个主题映射多个空格分隔的类名也是合法的),再把选中主题的类添加上去;
  3. 主题选择来源按优先级为:story 级 themeOverride 参数 → 工具栏选中的全局主题(globals.theme)→ defaultTheme

方案二:使用 data 属性切换主题(withThemeByDataAttribute

如果你把 Tailwind 的暗色模式配置成了 data 属性策略(例如 data-theme),则改用 withThemeByDataAttribute 装饰器:

-import { Preview } from '@storybook/your-renderer';
+import { Preview, Renderer } from '@storybook/your-renderer';
+import { withThemeByDataAttribute } from '@storybook/addon-themes';

import '../src/index.css';

const preview: Preview = {
  parameters: { /* ... */ },
+ decorators: [
+  withThemeByDataAttribute<Renderer>({
+    themes: {
+      light: 'light',
+      dark: 'dark',
+    },
+    defaultTheme: 'light',
+    attributeName: 'data-theme',
+  }),
+ ]
};

export default preview;

与类名方案相比,该装饰器(data-attribute.decorator.tsx)多一个 attributeName 配置项,默认为 'data-theme';其切换动作是对目标元素直接调用 setAttribute(attributeName, themes[themeKey]),即整体替换属性值,而不是逐个增删类。parentSelector 同样可选且默认指向 <html>。两种装饰器可依据你的 Tailwind darkMode: 'class' 或自定义 data-* 策略二选一。

工具栏如何联动:manager 侧实现

注册 addon 后无需任何额外代码,工具栏就会出现 "Themes" 下拉框。这由 manager.tsx 完成:它调用 addons.add(THEME_SWITCHER_ID, ...) 注册一个 types.TOOL 类型的控件,标题为 Themes,并且 match 条件限制了它仅在 storydocs 视图模式(且无 tab 激活时)显示——因此在欢迎页或其他页签中不会出现。

preview 侧的 preview.ts 则通过 initialGlobals 初始化全局状态 theme: '',与工具栏选中值共同构成 story 的默认主题来源。

进阶:按 story 覆盖主题与禁用插件

根据 READMEtypes.ts 中的 ThemesParameters / ThemesGlobals 类型定义,还有两类实用能力:

在 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 级覆盖
};

装饰器内部通过 themeOverride || selected || defaultTheme 的优先级处理该值(两个装饰器源码均可见此逻辑),因此被锁定的 story 会无视工具栏切换。

通过参数禁用插件——在 parameters.themes 中设置 disable: true 可移除工具栏面板并停用插件行为,themeOverride 亦可直接在此参数中声明。

小结

围绕 Tailwind 入门指南 的四步流程——安装、注册、引入 CSS、提供装饰器——即可让 Tailwind 组件在 Storybook 中实现 light/dark 一键切换。从源码层面看,其工作原理是:装饰器在 preview 侧向 <html> 元素应用/移除类或 data 属性,同时通过 REGISTER_THEMES 事件把主题列表上报给 manager 侧的 "Themes" 工具;globals.themeparameters.themes 则提供了全局与 story 级的主题锁定与禁用能力。如果你的项目使用 styled-components、emotion 或 MUI 等其他主题方案,仓库中 docs/getting-started 目录下还有对应的分工具指南(如 styled-components.mdmaterial-ui.md)可作参考;没有匹配的工具时,可依据 API 参考 中的 "Writing a custom decorator" 自行编写装饰器。

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