Storybook `@storybook/addon-themes`:用 styled-components 实现主题化 Story 与工具栏主题切换
本篇基于 Storybook 仓库中 themes add-on 的官方入门文档 styled-components 入门指南,完整讲解如何用 @storybook/addon-themes 为基于 styled-components 的组件库接入多主题支持:从安装包、注册 add-on,到用 withThemeFromJSXProvider 装饰器注入 ThemeProvider 与全局样式,并结合仓库源码说明主题选择的优先级链路、工具栏主题切换器的渲染逻辑,以及 themes 参数与 global 的用法,使读者既能照着落地,也能理解底层实现。
这个 add-on 解决什么问题
themes add-on(包名 @storybook/addon-themes)让组件 Story 可以声明多个主题,并在 Storybook 工具栏中提供一键切换能力。其核心导出位于 src/index.ts:默认导出一个 preview add-on(注入 initialGlobals),同时通过 export * from './decorators/index.ts' 把三个装饰器——withThemeFromJSXProvider、withThemeByClassName、withThemeByDataAttribute——公开给 preview 端使用。本文聚焦其中与 styled-components 生态配套的 withThemeFromJSXProvider。
安装 add-on
按照官方入门文档,先将包作为 dev 依赖安装。三种包管理器任选其一:
# yarn
yarn add -D @storybook/addon-themes
# npm
npm install -D @storybook/addon-themes
# pnpm
pnpm add -D @storybook/addon-themes
在 main 配置中注册 add-on
接下来在 .storybook/main.js 的 addons 数组中加入 add-on 标识,diff 如下(官方文档原文):
export default {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
'@storybook/addon-essentials',
+ '@storybook/addon-themes',
],
};
注册后,manager 端会在 src/manager.tsx 中以 story book/themes 作为 add-on ID 注册一个标题为 "Themes" 的 TOOL 类型工具项,其 match 条件为 viewMode 是 story 或 docs 且未处于子 tab,也就是说该切换器只在普通 Story / Docs 视图中出现。
提供主题:withThemeFromJSXProvider 装饰器
最后一步是把你的主题对象、ThemeProvider 和全局样式组件交给 add-on 提供的 withThemeFromJSXProvider 装饰器。按官方文档修改 .storybook/preview.js(注意 your-renderer 需替换为你实际使用的渲染器包,例如 react 对应 @storybook/react):
-import { Preview } from '@storybook/your-renderer';
+import { Preview, Renderer } from '@storybook/your-renderer';
+import { withThemeFromJSXProvider } from '@storybook/addon-themes';
+import { ThemeProvider } from 'styled-components';
+import { GlobalStyles, lightTheme, darkTheme } from '../src/themes'; // Import your custom theme configs
const preview: Preview = {
parameters: { /* ... */ },
+ decorators: [
+ withThemeFromJSXProvider<Renderer>({
+ themes: {
+ light: lightTheme,
+ dark: darkTheme,
+ },
+ defaultTheme: 'light',
+ Provider: ThemeProvider,
+ GlobalStyles: GlobalStyles,
+ }),
+ ],
};
export default preview;
其中 lightTheme、darkTheme 是你的主题 token 对象(例如颜色、字体、间距等),GlobalStyles 是一个把主题变量落到 CSS 变量或全局选择器上的 styled-components 组件。
参数详解
从源码 src/decorators/provider.decorator.tsx 中的 ProviderStrategyConfiguration 接口可以看到完整参数集:
| 参数 | 类型 | 说明 |
|---|---|---|
themes |
Record<string, Theme> |
主题名到主题对象的映射,例如 { light, dark };默认 {}。主题列表的 key 即工具栏中可选的主题名 |
defaultTheme |
string |
默认主题名。若省略,源码回退为 themeNames[0],即 themes 中第一个 key |
Provider |
任意 React 组件 | 主题 Provider 组件,例如 styled-components 的 ThemeProvider。装饰器会以 <Provider theme={theme}> 形式包裹 Story |
GlobalStyles |
任意 React 组件 | 全局样式组件,可选。渲染在 Provider 内部、storyFn() 之前,用于应用全局 CSS 变量等 |
主题选择逻辑:优先级与底层实现
阅读 provider.decorator.tsx 的实现,可以看清每次 Story 渲染时主题是如何被解析的:
-
装饰器创建阶段:
const initialTheme = defaultTheme || themeNames[0],随后调用initializeThemeState(themeNames, initialTheme)。查看 src/decorators/helpers.ts,它会通过 add-on channel 发出THEMING_EVENTS.REGISTER_THEMES事件,把主题名列表和默认主题上报给 manager——这正是工具栏切换器数据来源。 -
每次渲染阶段:从
context.parameters[PARAM_KEY](即parameters.themes)读取themeOverride,从globals[GLOBAL_KEY](global key 为字符串theme,定义见 src/constants.ts)读取用户当前选中的主题,最终主题按如下优先级解析:const selectedThemeName = themeOverride || selected || initialTheme;即:故事级
parameters.themes.themeOverride> 工具栏/global 选中值 > 默认主题。解析结果被useMemo缓存,仅依赖selected与themeOverride变化时重算。 -
单主题快捷路径:若
themes只有一个条目,直接取该主题对象,不做名称匹配。 -
无 Provider 的降级渲染:若未提供
Provider(例如纯 CSS 变量方案),装饰器不渲染包裹层,只输出{GlobalStyles && <GlobalStyles />}{storyFn()}——这解释了为何GlobalStyles对非 Provider 方案同样有价值。
preview 侧的配套初始状态见 src/preview.ts:它为 global theme 设置了空字符串的 initialGlobals,保证未选主题时不会读到 undefined。
工具栏主题切换器的行为
manager 端的 UI 实现在 src/theme-switcher.tsx,几个值得注意的行为细节:
- 形态随主题数量变化:恰好 2 个主题时渲染一个 ghost
Button(点击在两个主题间切换,带PaintBrushIcon图标);多于 2 个时渲染Select下拉框(见hasTwoThemes/hasMultipleThemes两个判断函数);少于 2 个则不渲染任何控件。 - 锁定(isLocked)状态:
const isLocked = KEY in storyGlobals || !!themeOverride——即某个 Story 通过全局配置固定了主题、或在参数中写了themeOverride时,切换器被disabled,按钮/下拉会显示 "Story override" 与 tooltip "Theme set by story parameters",提示当前主题由 Story 参数决定而非用户切换。 - 状态同步:切换器通过
useChannel监听REGISTER_THEMES事件更新themesList与themeDefault;用户选择后调用updateGlobals({ theme: selected }),把选中值写回 globaltheme,preview 端装饰器再经pluckThemeFromContext(helpers.ts)读回,完成一次闭环。 - 全局禁用:当参数
themes.disable为true时,组件直接return null,同时 src/types.ts 的注释表明该参数的语义是 "Remove the addon panel and disable the addon's behavior"。
themes 参数与 global 的补充用法
装饰器之外,add-on 还暴露了参数与 global 两个控制面(类型定义见 src/types.ts):
// Story 级:固定该 Story 的主题,并锁住工具栏切换
parameters: {
themes: {
disable: false, // 设为 true 时移除面板并停用 add-on 行为
themeOverride: 'dark', // 覆盖该 Story 使用的主题
},
},
// 全局/Story 级:预设选中的主题
globals: {
theme: 'light',
},
仓库中的模板示例 template/stories/parameters.stories.ts 与 template/stories/globals.stories.ts 分别演示了 themeOverride: 'b' 覆盖单个 Story 主题、globals: { theme: 'b' } 全局预设主题、以及 themes: { disable: true } 禁用 add-on 的完整写法;template/stories/decorators.stories.ts 则展示了三种装饰器的并存用法,可作为配置参考。
小结与延伸阅读
- 三步落地:安装
@storybook/addon-themes→ 在main.js注册 → 在preview.js用withThemeFromJSXProvider提供themes映射、ThemeProvider与GlobalStyles。 - 主题优先级为
themeOverride(Story 参数)>globals.theme(用户切换)>defaultTheme/首个主题名,这一链路直接体现在 provider.decorator.tsx 的一行解析逻辑中。 - 若项目不使用 styled-components,同一 add-on 还提供
withThemeByClassName与withThemeByDataAttribute等替代策略,对应文档位于 code/addons/themes/docs/getting-started/ 目录(emotion、material-ui、tailwind、bootstrap、postcss 等各有专页),参数级细节可进一步参阅 code/addons/themes/docs/api.md 与 code/addons/themes/README.md。
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 StartedRust0623
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