Storybook 背景网格(Backgrounds Grid)配置与实现原理详解
在 Storybook 中隔离开发组件时,"组件是否对齐、间距是否规整"这类问题经常要等到集成后才暴露。Storybook 的 Backgrounds 功能除了提供背景色切换外,还内置了一个网格(Grid)叠加层:把它叠加在画布上,就能即时检查组件与基础网格、布局边距是否吻合。本文以当前仓库(Storybook 主仓库)中的 addon-backgrounds-grid 代码片段 与 Backgrounds 官方文档 为骨架,结合 Backgrounds 模块源码,完整讲解 backgrounds.grid 参数体系中 cellSize、cellAmount、opacity、offsetX、offsetY、disable 等所有配置项,并深入还原网格在浏览器里到底是怎样被绘制出来的。读完你可以把网格一键接入任何组件、按设计稿微调网格间距,并理解每根网格线背后的 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 |
0(fullscreen)或 16(padded) |
网格水平方向的起始偏移,用于对齐画布内边距 |
offsetY |
number |
0(fullscreen)或 16(padded) |
网格垂直方向的起始偏移,规则同 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-csf 的 defineMeta,参数结构完全一致(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-vite、nextjs、vue3-vite、web-components-vite、sveltekit、angular等。
四、双级网格:cellSize 与 cellAmount 到底怎么组合
网格看起来只是"一组线",但源码里其实绘制了两组粗细/强弱不同的线,这就是 cellSize 与 cellAmount 分工的原因。看 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;
}
`;
解读这段实现:
- 四层背景渐变:前两层是水平/垂直的"主线",颜色为
rgba(130, 130, 130, ${opacity});后两层是水平/垂直的"次线",颜色为rgba(130, 130, 130, ${opacity / 2}),只有主线一半的透明度。 - 平铺单元:主线的
background-size是cellSize × cellAmount像素的方块,次线则是cellSize像素的方块。换句话说,基础细线每隔cellSize(默认 20px)出现一条;而每隔cellAmount个小格(即20 × 5 = 100px)会出现一条颜色更深的强调主格线。 - 混合模式:整层背景使用
background-blend-mode: difference。由于灰色线采用差值混合,无论故事底色是深还是浅,网格线都能保持可见,这正是网格可以在任意背景色上工作而不需要换色的原因。 - 偏移起点:所有层的
background-position都带offsetX/offsetY,从而让网格线可以从任意坐标开始排布。
因此可以把 cellSize 理解为"最小对齐单元",cellAmount 理解为"每多少个最小单元做一次强调划分"。
五、默认偏移量的精确规则:padded、fullscreen 与 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|ts 的 parameters.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.md,
grid对象与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.grid:boolean,控制网格是否显示;backgrounds.value:string,设置为某个背景色 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 },也支持简写字符串形式(此时等价于只指定背景色值)。
八、如何关闭网格
有两种关闭手段,语义不同:
- 临时关闭整个 Backgrounds 功能:设置
backgrounds.disable: true。disable默认在项目级注入为false,其价值在于可以按组件、按故事逐层覆盖——例如项目级关闭、某个组件重新开启。参考 addon-backgrounds-disabled.md。 - 仅关闭网格:设置
backgrounds.grid.disable: true,背景色选择器不受影响:
parameters: {
backgrounds: {
grid: {
disable: true,
},
},
}
九、样式注入的底层机制(为什么网格不会被背景色盖掉)
网格与背景色最终都是通过动态注入 <style> 标签实现的,这一点可以从 utils.ts 的 addGridStyle、addBackgroundStyle 与 clearStyle 三个函数看出:
- 每个样式都被赋予带命名空间的
<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 精确定位到对应故事的文档区块,避免一个页面上多个文档故事的网格互相干扰。
十、如何验证与继续探索
在当前仓库中验证本文结论可以分两条路径:
- 运行模板故事:
globals.stories.ts(位于 code/core/template/stories/backgrounds/)已经内置了Grid、GridConfig、GridOffset、GridAndBackground等可直接查看的网格示例,通过本地storybook dev启动后即可用工具栏的网格开关逐一观察效果差异。 - 端到端测试:Backgrounds 的浏览器级行为覆盖在 addon-backgrounds.spec.ts 中,可作为了解"哪些交互路径被官方保障"的参考。
希望进一步深入底层时,推荐按这个顺序阅读源码:
- types.ts:
GridConfig与参数/global 的类型契约; - constants.ts:addon id、
backgrounds参数 key、gridkey 等常量; - preview.ts:默认参数与默认 global 的注入点;
- decorator.ts:网格 CSS 的生成与作用域选择;
- utils.ts:样式节点的增删与层级控制。
这些文件与 docs/essentials/backgrounds.mdx、addon-backgrounds-grid.md 一起,构成了从"打开工具栏开关"到"一行行 CSS 渐变"的完整闭环。掌握之后,你不仅能配置出贴合设计稿的网格,也能在遇到"网格没对齐""网格被背景吃掉"之类的问题时,第一时间从样式注入与层叠顺序入手定位根因。
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 StartedRust0627
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