首页
/ Storybook 背景网格(Backgrounds Grid)配置与实现原理详解

Storybook 背景网格(Backgrounds Grid)配置与实现原理详解

2026-09-06 18:57:48作者:庞眉杨Will

在 Storybook 中隔离开发组件时,"组件是否对齐、间距是否规整"这类问题经常要等到集成后才暴露。Storybook 的 Backgrounds 功能除了提供背景色切换外,还内置了一个网格(Grid)叠加层:把它叠加在画布上,就能即时检查组件与基础网格、布局边距是否吻合。本文以当前仓库(Storybook 主仓库)中的 addon-backgrounds-grid 代码片段Backgrounds 官方文档 为骨架,结合 Backgrounds 模块源码,完整讲解 backgrounds.grid 参数体系中 cellSizecellAmountopacityoffsetXoffsetYdisable 等所有配置项,并深入还原网格在浏览器里到底是怎样被绘制出来的。读完你可以把网格一键接入任何组件、按设计稿微调网格间距,并理解每根网格线背后的 CSS 计算逻辑。

一、网格是什么,解决什么问题

Backgrounds 功能在工具栏里提供两个相互独立的开关:

  • 背景色选择器:在预设的背景色中切换,故事画布随之换底色;
  • 网格选择器:在画布上叠加一组灰白网格线,帮助快速判断组件是否在基准线上对齐

网格的启用状态由 backgrounds.grid 这个 global(boolean 类型)控制,默认关闭;一旦打开,网格会通过 CSS 背景图层直接叠加在故事预览区域之上,并不影响组件自身的 DOM 结构与样式。

从功能定位看,它适合这些场景:

  • 用等宽单元格核验列表、栅格类组件的行高与间距是否一致;
  • 打开网格后逐帧观察动画/悬浮态是否发生位移;
  • 检查页面级组件(layout 为 fullscreen)是否与设计稿的 8px/20px 网格体系吻合。

二、配置项速览:一张表看懂 backgrounds.grid

按官方文档的 API 描述,grid 参数支持以下属性。你不需要任何额外配置即可使用默认网格,所有属性都支持按需覆盖:

属性 类型 默认值 作用
cellAmount number 5 多少个 cellSize 出现一条强调的主网格线(见下文"双级网格"实现)
cellSize number 20 基础网格单元边长(px),决定最细网格线的间距
disable boolean false 关闭网格
offsetX number 0fullscreen)或 16padded 网格水平方向的起始偏移,用于对齐画布内边距
offsetY number 0fullscreen)或 16padded 网格垂直方向的起始偏移,规则同 offsetX
opacity number 0.5 网格线的不透明度

说明:默认 offsetX/offsetY 之所以与布局相关,是因为 Storybook 预览区默认采用 padded 布局(四周 16px 内边距)。如果故事声明了 layout: 'fullscreen'(无内边距),网格默认就从画布原点 (0,0) 开始绘制,从而让网格与内容区域对齐。如果你希望网格对齐到内容而非画布,通常需要保持默认值即可;手动指定 offsetX/offsetY 时网格会以该坐标作为绘制起点。

这套默认值并非写死在文档里,而是由模块运行时注入的真实参数。在 preview.ts 中可以看到:

const parameters = {
  [PARAM_KEY]: {
    grid: {
      cellSize: 20,
      opacity: 0.5,
      cellAmount: 5,
    },
    disable: false,
  },
} satisfies Partial<BackgroundsParameters>;

const initialGlobals: Record<string, GlobalState> = {
  [PARAM_KEY]: { value: undefined, grid: false },
};

也就是说每个项目默认都拥有 cellSize: 20 / opacity: 0.5 / cellAmount: 5 的网格参数,且网格 global 初始为 false(关闭),背景色 global 初始为 undefined。对应 TypeScript 类型定义见 types.ts 中的 GridConfig

三、在组件元数据中配置网格(核心用法)

代码片段 addon-backgrounds-grid.md 演示的正是最常用的一层:在 meta(组件级) 声明网格参数,让该组件的所有 story 共享同一套网格配置。之所以放在 meta.parameters 而不是每个 story 上重复声明,是因为 Storybook 的 parameters 会沿"项目 → 组件 → 故事"逐级合并覆盖。

下面是完整的 TypeScript + CSF 3 写法(配置项与原片段完全一致,注释一并保留):

// Button.stories.ts
import type { Meta } from '@storybook/your-framework';
import { Button } from './Button';

// To apply a set of backgrounds to all stories of Button:
const meta = {
  component: Button,
  parameters: {
    backgrounds: {
      grid: {
        cellSize: 20,
        opacity: 0.5,
        cellAmount: 5,
        offsetX: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
        offsetY: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
      },
    },
  },
} satisfies Meta<typeof Button>;

export default meta;

如果项目中启用了实验性的 CSF Next API(基于 .storybook/preview 导出的 preview.meta()),等价写法为:

// Button.stories.ts
import preview from '../.storybook/preview';
import { Button } from './Button';

const meta = preview.meta({
  component: Button,
  parameters: {
    backgrounds: {
      grid: {
        cellSize: 20,
        opacity: 0.5,
        cellAmount: 5,
        offsetX: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
        offsetY: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
      },
    },
  },
});

在 Svelte 场景下,使用 @storybook/addon-svelte-csfdefineMeta,参数结构完全一致(script module 块内声明):

<!-- Button.stories.svelte -->
<script module>
  import { defineMeta } from '@storybook/addon-svelte-csf';
  import Button from './Button.svelte';

  const { Story } = defineMeta({
    component: Button,
    parameters: {
      backgrounds: {
        grid: {
          cellSize: 20,
          opacity: 0.5,
          cellAmount: 5,
          offsetX: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
          offsetY: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
        },
      },
    },
  });
</script>

其余渲染器的语法差异只体现在两处,配置对象本身不变:

  • 组件引用方式:React 引 Button 组件;Web Components 把组件写成标签名 component: 'demo-button';Vue 引 Button.vue;Svelte 引 Button.svelte
  • 类型来源import type { Meta } from '@storybook/<framework>',按实际框架替换为 react-vitenextjsvue3-viteweb-components-vitesveltekitangular 等。

四、双级网格:cellSizecellAmount 到底怎么组合

网格看起来只是"一组线",但源码里其实绘制了两组粗细/强弱不同的线,这就是 cellSizecellAmount 分工的原因。看 decorator.ts 中真正生成 CSS 的部分:

const gridSize = [
  `${cellSize * cellAmount}px ${cellSize * cellAmount}px`,
  `${cellSize * cellAmount}px ${cellSize * cellAmount}px`,
  `${cellSize}px ${cellSize}px`,
  `${cellSize}px ${cellSize}px`,
].join(', ');

const gridStyles = `
  ${gridSelector} {
    background-size: ${gridSize} !important;
    background-position: ${offsetX}px ${offsetY}px, ... !important;
    background-blend-mode: difference !important;
    background-image: linear-gradient(rgba(130, 130, 130, ${opacity}) 1px, transparent 1px),
      linear-gradient(90deg, rgba(130, 130, 130, ${opacity}) 1px, transparent 1px),
      linear-gradient(rgba(130, 130, 130, ${opacity / 2}) 1px, transparent 1px),
      linear-gradient(90deg, rgba(130, 130, 130, ${opacity / 2}) 1px, transparent 1px) !important;
  }
`;

解读这段实现:

  1. 四层背景渐变:前两层是水平/垂直的"主线",颜色为 rgba(130, 130, 130, ${opacity});后两层是水平/垂直的"次线",颜色为 rgba(130, 130, 130, ${opacity / 2}),只有主线一半的透明度。
  2. 平铺单元:主线的 background-sizecellSize × cellAmount 像素的方块,次线则是 cellSize 像素的方块。换句话说,基础细线每隔 cellSize(默认 20px)出现一条;而每隔 cellAmount 个小格(即 20 × 5 = 100px)会出现一条颜色更深的强调主格线。
  3. 混合模式:整层背景使用 background-blend-mode: difference。由于灰色线采用差值混合,无论故事底色是深还是浅,网格线都能保持可见,这正是网格可以在任意背景色上工作而不需要换色的原因。
  4. 偏移起点:所有层的 background-position 都带 offsetX/offsetY,从而让网格线可以从任意坐标开始排布。

因此可以把 cellSize 理解为"最小对齐单元",cellAmount 理解为"每多少个最小单元做一次强调划分"。

五、默认偏移量的精确规则:paddedfullscreen 与 Docs 模式

offsetX/offsetY 的默认值并非固定常量,而是由故事当前的 layout 参数与视图模式共同决定的。同样在 decorator.ts

const isLayoutPadded = parameters.layout === undefined || parameters.layout === 'padded';
const defaultOffset = viewMode === 'docs' ? 20 : isLayoutPadded ? 16 : 0;
const { cellAmount, cellSize, opacity, offsetX = defaultOffset, offsetY = defaultOffset } = grid;

据此可以整理出精确规则:

  • 故事没有显式声明 layout,或 layout: 'padded'(默认布局):默认偏移 16px,正好匹配预览内容区四周的内边距;
  • 故事声明 layout: 'fullscreen':默认偏移 0px,网格从画布原点开始;
  • 处于 Docs 模式(文档页内嵌的故事)时:默认偏移为 20px,与 Docs 页面的内边距体系对齐。

也就是说,只有当你想让网格相对这套默认规则再平移(比如缩进一列、对齐某个特定基准线)时,才需要显式传入 offsetX/offsetY

六、不同层级如何配置:全局 / 组件 / 单故事

网格参数可以像其他 backgrounds 参数一样放在三个层级,实现"全局默认 + 局部覆盖"。

6.1 项目级:修改所有故事的网格默认值

.storybook/preview.js|tsparameters.backgrounds 中与 options 平级声明 grid,即可修改全局默认网格。例如结合背景色选项配置一个 10px 网格:

// .storybook/preview.js
export const parameters = {
  backgrounds: {
    values: [
      { name: 'light', value: '#F8F8F8' },
      { name: 'dark', value: '#333' },
    ],
    grid: {
      cellSize: 10,
      cellAmount: 8,
      opacity: 0.4,
    },
  },
};

背景色选项的完整配置示例见 addon-backgrounds-options-in-preview.mdgrid 对象与 options 同属 backgrounds 命名空间。

6.2 组件级:某个组件的所有故事统一使用同一网格

即本文第三节展示的 meta.parameters.backgrounds.grid 写法,无需重复。对同一目录下多个 stories 文件的批量设置,参照 addon-backgrounds-options-in-meta.md 的思路在 meta 层做一次声明即可。

6.3 故事级:仅对单个故事微调

将配置放进单个导出的 story 对象:

export const GridAlignedStory = {
  parameters: {
    backgrounds: {
      grid: {
        cellSize: 8,
        opacity: 0.6,
        offsetX: 0,
        offsetY: 0,
      },
    },
  },
};

由于 Storybook 对 parameters 做按层合并,故事级对象里的 grid 会整体覆盖组件级/项目级的 grid 配置。

七、组合使用:网格 + 固定背景色

网格常与"锁定背景色"配合使用。Backgrounds 模块对外暴露两个 global(见 backgrounds.mdx 的 API 章节):

  • backgrounds.gridboolean,控制网格是否显示;
  • backgrounds.valuestring,设置为某个背景色 key 后,该故事将固定在该背景上渲染,工具栏无法再切换(适用于必须验证深色背景下的对齐)。

在仓库自带的模板验证故事 globals.stories.ts 中可以找到各种组合的官方样例:

// 只开网格,背景保持默认
export const Grid = {
  globals: {
    backgrounds: { grid: true },
  },
};

// 深色背景 + 网格
export const GridAndBackground = {
  globals: {
    backgrounds: { grid: true, value: 'darker' },
  },
};

// 覆盖网格参数后开启
export const GridConfig = {
  parameters: {
    backgrounds: {
      grid: {
        cellSize: 100,
        cellAmount: 10,
        opacity: 0.8,
      },
    },
  },
  globals: {
    backgrounds: { grid: true, value: 'light' },
  },
};

// 指定偏移
export const GridOffset = {
  parameters: {
    backgrounds: {
      grid: {
        cellSize: 100,
        cellAmount: 10,
        opacity: 0.8,
        offsetX: 50,
        offsetY: 50,
      },
    },
  },
  globals: {
    backgrounds: { grid: true, value: 'light' },
  },
};

从这些样例可以看出:在某个 story 上固定启用网格最直接的方式,就是通过 globals.backgrounds.grid: true 声明;backgrounds 的 global 值既支持对象形式 { value, grid },也支持简写字符串形式(此时等价于只指定背景色值)。

八、如何关闭网格

有两种关闭手段,语义不同:

  1. 临时关闭整个 Backgrounds 功能:设置 backgrounds.disable: truedisable 默认在项目级注入为 false,其价值在于可以按组件、按故事逐层覆盖——例如项目级关闭、某个组件重新开启。参考 addon-backgrounds-disabled.md
  2. 仅关闭网格:设置 backgrounds.grid.disable: true,背景色选择器不受影响:
parameters: {
  backgrounds: {
    grid: {
      disable: true,
    },
  },
}

九、样式注入的底层机制(为什么网格不会被背景色盖掉)

网格与背景色最终都是通过动态注入 <style> 标签实现的,这一点可以从 utils.tsaddGridStyleaddBackgroundStyleclearStyle 三个函数看出:

  • 每个样式都被赋予带命名空间的 <style id="addon-backgrounds-..."> 节点,插入 document.head
  • 当对应 global 被关闭(如 showGrid 为假)时,调用 clearStyles 移除该样式节点,而不是留一个空样式;
  • 当网格与背景色同时开启时,代码会把背景色样式节点插入到网格样式节点之前(见 utils.ts 的注释 "the background doesn't override grid")。由于 CSS 层叠中靠后的规则胜出,网格样式总是位于背景色样式之后,从而保证网格永远压在背景色上层。

值得注意的还有两点渲染细节(见 decorator.ts):

  • 背景色切换默认带有 transition: background-color 0.3s 的平滑过渡;
  • 若用户系统开启了"减弱动态效果"(prefers-reduced-motion: reduce,通过 matchMedia 检测),则自动跳过该过渡,避免动画干扰。

此外,网格样式的作用域会随视图模式变化:普通 story 模式作用于 .sb-show-main 预览根节点;Docs 模式则通过 #anchor--${id} .docs-story 精确定位到对应故事的文档区块,避免一个页面上多个文档故事的网格互相干扰。

十、如何验证与继续探索

在当前仓库中验证本文结论可以分两条路径:

  1. 运行模板故事globals.stories.ts(位于 code/core/template/stories/backgrounds/)已经内置了 GridGridConfigGridOffsetGridAndBackground 等可直接查看的网格示例,通过本地 storybook dev 启动后即可用工具栏的网格开关逐一观察效果差异。
  2. 端到端测试:Backgrounds 的浏览器级行为覆盖在 addon-backgrounds.spec.ts 中,可作为了解"哪些交互路径被官方保障"的参考。

希望进一步深入底层时,推荐按这个顺序阅读源码:

  • types.tsGridConfig 与参数/global 的类型契约;
  • constants.ts:addon id、backgrounds 参数 key、grid key 等常量;
  • preview.ts:默认参数与默认 global 的注入点;
  • decorator.ts:网格 CSS 的生成与作用域选择;
  • utils.ts:样式节点的增删与层级控制。

这些文件与 docs/essentials/backgrounds.mdxaddon-backgrounds-grid.md 一起,构成了从"打开工具栏开关"到"一行行 CSS 渐变"的完整闭环。掌握之后,你不仅能配置出贴合设计稿的网格,也能在遇到"网格没对齐""网格被背景吃掉"之类的问题时,第一时间从样式注入与层叠顺序入手定位根因。

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