首页
/ Storybook Docs 主题定制全解:parameters.docs.theme、CSS 逃生舱与 MDX 组件覆盖三级机制

Storybook Docs 主题定制全解:parameters.docs.theme、CSS 逃生舱与 MDX 组件覆盖三级机制

2026-09-06 12:46:49作者:尤峻淳Whitney

Storybook 的 Docs 功能(@storybook/addon-docs)支持完整的主题定制。本篇以仓库中的文档 code/addons/docs/docs/theming.md 为核心,系统讲解 Docs 的三级主题定制机制:官方推荐的 parameters.docs.theme 主题变量、基于 sbdocs-* 类名的 CSS 逃生舱、以及 MDX components 参数级别的组件覆盖,并结合源码还原每一级机制在渲染链路中的真实落点,帮助你在实际项目中精确控制 Docs 页面的视觉表现。

三级主题机制总览

Docs 功能文档 指出,Storybook Docs 是可主题化的,并且刻意提供了三个不同层次的定制入口,以便按“侵入程度”逐级升级:

  1. Storybook theming(推荐):复用 Storybook 的统一主题系统(@storybook/theming),但 Docs 主题与主 UI(Manager 侧)主题相互独立、互不干扰;
  2. CSS escape hatches:当主题 API 不够用时,通过 sbdocs-* 类名直接编写 CSS 微调样式(高级用法,风险自负);
  3. MDX component overrides:借助 MDX 的 components 参数彻底替换文档中渲染的组件,甚至包括 Storybook 自带的 Doc Block(高级用法,官方不做支持承诺)。

理解这三层的价值在于:绝大多数场景只需第一层;只有第一层的变量体系覆盖不到时,才向下逐级升级,避免过早引入脆弱的类名依赖。

第一级:用 parameters.docs.theme 指定 Docs 主题

Docs 主题与主 UI 主题相互独立

Docs 使用与 Storybook UI 相同(同一套 @storybook/theming 主题系统),但独立于主 UI 进行主题化。这一点在源码中有直接体现:渲染 Docs 页面时,主题并不是取自 Manager 侧的全局主题,而是从 Docs 参数中单独取出的 docsParameter.theme

Docs 组件 展示了这一解耦:

export function Docs<TRenderer extends Renderer = Renderer>({
  context,
  docsParameter,
}: DocsProps<TRenderer>) {
  const Container: ComponentType<...> = docsParameter.container || DocsContainer;

  const Page = docsParameter.page || DocsPage;

  return (
    <Container context={context} theme={docsParameter.theme}>
      <Page />
    </Container>
  );
}

随后 DocsContainer 将该主题经 ensureTheme 规范化后注入独立的 ThemeProvider

<ThemeProvider theme={ensureTheme(theme as ThemeVars)}>
  <DocsPageWrapper lang={lang} toc={...}>
    {children}
  </DocsPageWrapper>
</ThemeProvider>

因此可以推断:即使 Manager(导航栏、侧边栏)是深色主题,Docs 页面仍可以是浅色主题,反之亦然——两者的主题变量在运行时走的是两条不同的注入链路。

配置方式:manager.js 与 preview.js 各自定义

原文档给出的完整配置示例如下。假设你已经在 .storybook/manager.js 中为主 UI 指定了主题:

// .storybook/manager.js
// or a custom theme
import { themes } from '@storybook/theming';
import { addons } from '@storybook/manager-api';

addons.setConfig({
  theme: themes.dark,
});

那么为 Docs 指定同一主题的做法,是在 .storybook/preview.js 中通过 parameters.docs.theme

// .storybook/preview.js
import { themes } from '@storybook/theming';

// or global addParameters
export const parameters = {
  docs: {
    theme: themes.dark,
  },
};

docs 参数类型定义在 types.ts 中,注释明确其用途就是 “Override the default theme”(覆盖默认主题):

/**
 * Override the default theme
 */
theme?: ThemeVars;

可用的内置主题与自定义主题

从源码 create.ts 可以看到,themes 对象包含三个内置入口:

export const themes: Themes = {
  ...themesBase,        // light: 浅色主题变量, dark: 深色主题变量
  normal: themesBase[preferredColorScheme], // 跟随系统偏好
};
  • themes.light / themes.dark:固定的明暗两套 ThemeVars
  • themes.normal:运行时读取浏览器/系统的首选色彩方案(getPreferredColorScheme()),自动跟随。

如果内置主题不够,同一文件中的 create() 函数(create.ts)提供了合并语义的自定义主题 API:先继承系统偏好主题,再叠加声明的 base 主题,最后叠加你自己的变量覆盖,并自动兜底 barSelectedColor。也就是说,自定义 Docs 主题只需写差异化的变量即可,无需手写完整的 ThemeVars

// .storybook/preview.js
import { create, themes } from '@storybook/theming';

const brandTheme = create(themes.light, {
  appContentBackground: '#f7f7fa',
  fontBase: '"Inter", sans-serif',
  colorPrimary: '#4f46e5',
});

export const parameters = {
  docs: {
    theme: brandTheme,
  },
};

作用范围上,parameters.docs.theme 遵循 Storybook 参数体系的继承链:可以写在全局 preview 配置中,也可以写入某个 story 文件(meta 级)或单个 story 的 parameters 中做局部覆盖。从源码结构看,Docs 组件接收到的 docsParameter 即该 Docs 上下文下的参数集合,因此页面级、故事级的覆盖都能落到同一条渲染链路上。

第二级:CSS 逃生舱(CSS escape hatches)

原文档开宗明义:Storybook 的主题 API 在设计上就是窄的。当你需要对 CSS 做细粒度控制时,所有 Docs 组件都带上了类名标记,使直接写 CSS 成为可能。这是高级用法,需自行承担兼容性风险。

类名的两条来源

类名分为两类,分别对应 Markdown 元素与页面 UI 元素:

  • Markdown 元素类sbdocs-titlesbdocs-subtitlesbdocs-p 等;
  • 页面 UI 元素类sbdocs-containersbdocs-contentsbdocs-wrapper 等。

Markdown 类名由通用工具 nameSpaceClassNames 统一注入——它把任意排版组件(H1H6preahr 等)的 className 归一化为 sbdocs sbdocs-{element} ...

export const nameSpaceClassNames = ({ ...props }, key: string) => {
  const classes = [props.class, props.className];
  delete props.class;
  props.className = ['sbdocs', `sbdocs-${key}`, ...classes].filter(Boolean).join(' ');
  return props;
};

Storybook 自绘的标题组件同理,例如 Title 组件 输出 sbdocs-title sb-unstyledSubtitle 组件 输出 sbdocs-subtitle sb-unstyled。页面骨架类名则直接写死在 DocsPage:外层容器是 sbdocs sbdocs-wrapper,内容区是 sbdocs sbdocs-content。要查看当前版本下实际可用的类名,原文档建议直接用浏览器的 “Inspect Element” 检查页面,这是最可靠的依据。

在 preview-head.html 中注入自定义 CSS

这些类名可以在 .storybook/preview-head.html 中样式化。原文档给出的示例是面向 UHD 屏幕加宽内容区:

<!-- .storybook/preview-head.html -->
<style>
  .sbdocs.sbdocs-content {
    max-width: 1440px;
  }
</style>

NOTE:所有这些元素同时带有 sbdocs 类,这是提升 CSS 特异性的惯用写法——.sbdocs.sbdocs-content 的双重类选择器可以稳定压过 Storybook 默认样式,让你不必使用 !important

源码侧为何“容易”被覆盖

从源码结构看,这个逃生舱能被低成本使用并非偶然。DocsPage 对原始元素(div、p、ul 等)的默认样式全部包在零特异性选择器里:

// ':where': ensures this has a specificity of 0, making it easier to override.
const toGlobalSelector = (element: string): string =>
  `& :where(${element}:not(.sb-anchor, .sb-unstyled, .sb-unstyled ${element}))`;

:where() 使默认排版样式特异性为 0,你的 preview-head.html 样式天然占优。此外还存在一个更彻底的出口:注释中说明的 sb-unstyled 类(或 <Unstyled /> block)可让整段内容完全退出 Docs 默认样式体系——如果你要在 Docs 页面嵌入自己完全接管样式的组件,这是比写 CSS 更干净的方案。

第三级:MDX 组件覆盖(MDX component overrides)

在使用 MDX 时还有最后一层主题能力:MDX 允许通过 components 参数彻底替换由 Markdown 渲染出的组件。原文档明确标注:这是高级用法,Storybook 官方不做正式支持,但机制本身非常强大。

覆盖机制在渲染链中的位置

这一级的底层落点在 DocsRenderer。Storybook 先定义一组默认组件:

export const defaultComponents: Record<string, any> = {
  code: CodeOrSourceMdx,
  a: AnchorMdx,
  ...HeadersMdx, // h1~h6
};

随后在渲染函数里把默认组件与你在 parameters.docs.components 中声明的组件做浅合并,再交给 MDXProvider

const components = {
  ...defaultComponents,
  ...docsParameter?.components,
};
// ...
<MDXProvider components={components}>
  <TDocs context={context} docsParameter={docsParameter} />
</MDXProvider>

由于用户声明放在展开顺序的后面,docs.components 中的同名键会精确覆盖对应默认组件,且未覆盖的键(比如你只重写 code 时)仍回退到 Storybook 默认实现。

示例一:自定义 code 代码块渲染器

原文档示例——在 .storybook/preview.js 中插入自定义 code 渲染器:

import { addParameters } from '@storybook/react';
import { CodeBlock } from './CodeBlock';

addParameters({
  docs: {
    components: {
      code: CodeBlock,
    },
  },
});

被覆盖的默认实现 CodeOrSourceMdx 的逻辑值得了解:内联代码(无 className 且无换行)渲染为 Code 内联样式;带语言标记的代码块(如 lang-jsx)则渲染为完整的 Source 组件(带语言解析与复制能力)。你用自己的 CodeBlock 替换它时,即接管了这两种形态的渲染。

示例二:覆盖 Storybook 的 Block 组件

覆盖能力不限于 Markdown 元素,还可以覆盖 Storybook 自身的 Doc Block 组件。原文档示例——插入自定义 <Preview /> 块:

import { MyPreview } from './MyPreview';

addParameters({
  docs: {
    components: {
      Preview: MyPreview,
    },
  },
});

这与第一、二级形成了清晰的分工:主题变量控制“颜色、字体、间距”这类连续值,CSS 逃生舱控制“某个具体元素的样式”,而组件覆盖直接替换“哪个 React 组件负责渲染”——三者侵入程度依次加深,按需选择即可。

小结与延伸阅读

层级 入口 适用场景 风险
Storybook theming parameters.docs.theme 换主题、换品牌色、换字体 低,官方推荐
CSS escape hatches sbdocs-* 类名 + .storybook/preview-head.html 主题变量覆盖不到的单点样式 中,类名属内部实现细节
MDX component overrides parameters.docs.components 完全自定义代码块、锚点、Block 渲染 高,官方不做支持承诺

想继续深入 Docs 的其他方面,可参阅仓库中同一目录下的文档:Docs READMEDocsPageMDXFAQRecipesProps 表格

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