当设计稿的变量遇上代码的变量:从 Figma 到 CSS Variable 的管道为何总是断流 当设计稿的变量遇上代码的变量从 Figma 到 CSS Variable 的管道为何总是断流一、深度引言与场景痛点设计 Token 是设计系统的心跳——每一组颜色、间距、字号都是一个脉搏信号从设计工具传向代码仓库驱动整个产品界面的统一节奏。Figma 里的变量定义得再漂亮如果传不到 CSS 代码里那脉搏就断了。断流的痛点不是能不能传而是怎么传才不漏、不乱、不慢。手动导出 JSON 再手动写 CSS Variable设计师改一次颜色开发者就得手动改一次代码中间有无数个忘了更新的时刻。自动导出工具能解决速度问题但映射规则、命名对齐、多主题同步这些结构性问题才是管道真正容易堵塞的地方。这篇文章要搭建的管道从 Figma Variables API 自动提取设计 Token经过语义映射层转换命名格式输出为 CSS 自定义属性文件并自动提交到组件库仓库。管道全程自动化设计师在 Figma 中修改 Token代码仓库在几分钟内同步更新。二、底层机制与原理深度剖析Figma 到 CSS Variable 的管道由三个核心阶段组成提取、映射、输出。每个阶段处理一类特定问题逐层把设计意图转化为可执行的代码声明。flowchart LR A[Figma Variables API] -- B[提取层] B -- B1[遍历 Variable Collections] B -- B2[读取每个 Variable 的值与类型] B -- B3[提取模式 Mode 映射关系] B1 B2 B3 -- C[映射层] C -- C1[命名格式转换figma-name → kebab-case] C -- C2[语义层级重构primitive → semantic] C -- C3[别名关系解析Variable 引用链展开] C1 C2 C3 -- D[输出层] D -- D1[生成 tokens.cssCSS 自定义属性] D -- D2[生成 tokens.json跨平台共享源] D -- D3[Git 提交触发 CI 同步] D1 -- E[组件库样式入口] D2 -- F[iOS / Android 转换管道]提取层通过 Figma REST API 访问 Variables 数据。每个 Collection 是一个 Token 分组比如Global Colors、Spacing每个 Variable 有name、value、variableType三个核心字段。Figma 支持 Mode模式机制——一个 Variable 可以在不同 Mode 下有不同值这正是暗色模式的天然容器同一个background-primaryVariable在LightMode 下是白色在DarkMode 下是深灰。映射层解决命名对齐问题。Figma 的变量命名使用/作为层级分隔符如Colors/Primary/Blue500而 CSS Variable 使用-连接如--colors-primary-blue-500。映射层还负责语义层级重构——Figma 中设计师可能定义的是原始色值primitive但 CSS 中需要语义引用semantic比如--color-background-primary应该引用--color-blue-500而不是直接写值。这层映射确保了设计意图的语义传递。输出层将映射后的 Token 写入两种格式CSS 自定义属性文件供 Web 端直接使用JSON 文件供 iOS/Android 端的转换管道消费。输出后通过 Git 自动提交触发 CI确保代码仓库始终与 Figma 同步。三、生产级代码实现与最佳实践Figma Token 提取脚本// scripts/figma-token-extract.ts import { FigmaClient } from ./figma-client; interface FigmaVariable { id: string; name: string; variableType: COLOR | FLOAT | STRING | BOOLEAN; resolvedType: string; valuesByMode: Recordstring, string | number | { r: number; g: number; b: number; a: number }; aliasesByMode?: Recordstring, string; // 引用其他 Variable 的 ID } interface TokenOutput { name: string; // 语义化 CSS 命名 value: string; // 最终值或 CSS 引用表达式 type: color | spacing | font-size | font-weight | opacity | border-radius; figmaName: string; // 原始 Figma 命名用于溯源 mode: string; // 所属模式Light / Dark } // Figma 文件 key 和项目 Token Collection ID const FIGMA_FILE_KEY process.env.FIGMA_FILE_KEY!; const FIGMA_TOKEN_COLLECTIONS [Global Colors, Spacing, Typography]; async function extractTokens(): PromiseTokenOutput[] { const client new FigmaClient(process.env.FIGMA_ACCESS_TOKEN!); const tokens: TokenOutput[] []; // 获取文件中所有 Variable Collections const collections await client.getVariableCollections(FIGMA_FILE_KEY); for (const collectionName of FIGMA_TOKEN_COLLECTIONS) { const collection collections.find(c c.name collectionName); if (!collection) continue; // 获取 Collection 下的所有 Variables const variables await client.getVariablesByCollection( FIGMA_FILE_KEY, collection.id ); // 遍历每个 Mode通常是 Light 和 Dark for (const [modeId, modeName] of Object.entries(collection.modes)) { for (const variable of variables) { const modeValue variable.valuesByMode[modeId]; // 如果值是引用alias先解析引用链 const resolvedValue resolveAlias(variable, modeId, variables); tokens.push({ // Figma 的 / 分隔符转为 CSS 的 - 连接符 name: figmaNameToCssName(variable.name), value: formatValue(resolvedValue, variable.variableType), type: figmaTypeToTokenType(variable.variableType), figmaName: variable.name, mode: modeName }); } } } return tokens; } // Figma 命名转 CSS Variable 命名 function figmaNameToCssName(figmaName: string): string { // Colors/Primary/Blue500 → --colors-primary-blue-500 const segments figmaName .split(/) .map(s s.replace(/([a-z])([A-Z])/g, $1-$2).toLowerCase()); return --${segments.join(-)}; } // 格式化 Token 值为 CSS 合法格式 function formatValue(value: any, type: string): string { if (type COLOR typeof value object) { // Figma 颜色值为 {r, g, b, a}转为 rgba() const { r, g, b, a } value; return rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a}); } if (type FLOAT) { // 间距等数值 Token带单位 return ${value}px; } return String(value); } // 解析 Variable 引用链确保语义 Token 引用原始 Token function resolveAlias( variable: FigmaVariable, modeId: string, allVariables: FigmaVariable[] ): any { const aliasId variable.aliasesByMode?.[modeId]; if (!aliasId) return variable.valuesByMode[modeId]; // 递归解析引用直到找到最终值 const referenced allVariables.find(v v.id aliasId); if (!referenced) return variable.valuesByMode[modeId]; return resolveAlias(referenced, modeId, allVariables); } function figmaTypeToTokenType(type: string): TokenOutput[type] { const map: Recordstring, TokenOutput[type] { COLOR: color, FLOAT: spacing, STRING: font-size, }; return map[type] || spacing; }CSS Variable 输出生成// scripts/generate-css-tokens.ts import { extractTokens } from ./figma-token-extract; import fs from fs; async function generateCssTokens() { const tokens await extractTokens(); // 按 Mode 分组Light 和 Dark 分别输出 const lightTokens tokens.filter(t t.mode Light); const darkTokens tokens.filter(t t.mode Dark); // Light 模式默认值声明 const lightCss lightTokens.map(t ${t.name}: ${t.value}; /* 源: ${t.figmaName} */ ).join(\n); // Dark 模式覆盖声明 const darkCss darkTokens.map(t ${t.name}: ${t.value}; /* 源: ${t.figmaName} */ ).join(\n); const output /* 设计 Token — 自动生成勿手动编辑 */ /* 生成时间: ${new Date().toISOString()} */ /* Figma 源文件: ${process.env.FIGMA_FILE_KEY} */ :root { ${lightCss} } [data-themedark] { ${darkCss} } ; fs.writeFileSync(src/styles/tokens.css, output); console.log(✅ 已生成 ${tokens.length} 个 Token 至 tokens.css); } generateCssTokens();CI 自动同步 Git 提交# .github/workflows/token-sync.yml name: Design Token Sync on: schedule: - cron: 0 */4 * * * # 每4小时自动同步 workflow_dispatch: # 手动触发同步 jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm ci # 从 Figma 提取并生成 Token 文件 - run: node scripts/generate-css-tokens.ts env: FIGMA_ACCESS_TOKEN: ${{ secrets.FIGMA_ACCESS_TOKEN }} FIGMA_FILE_KEY: ${{ secrets.FIGMA_FILE_KEY }} # 检测是否有变更 - id: changes run: | if git diff --quiet src/styles/tokens.css; then echo has_changesfalse $GITHUB_OUTPUT else echo has_changestrue $GITHUB_OUTPUT fi # 有变更时自动提交 - if: steps.changes.outputs.has_changes true run: | git config user.name Design Token Bot git config user.email token-botdesign-system.dev git add src/styles/tokens.css git commit -m sync: 更新设计 Token from Figma git push四、边界分析与架构权衡Figma API 的速率限制。REST API 每分钟最多 60 次请求大型项目的 Token 提取可能需要多次分页请求。CI 定时同步间隔设为 4 小时是合理的平衡点更频繁的同步会撞上速率墙。如果需要即时同步可以在 Figma 端设置 Webhook 插件在 Variable 修改时主动推送变更信号。命名映射的语义断层。Figma 中设计师的命名习惯是视觉导向的Blue500而 CSS Variable 需要语义导向的命名--color-background-primary。自动转换脚本只能做格式转换/→-语义重构需要额外的映射配置文件。这个映射文件本身需要人工维护——哪个原始色值对应哪个语义用途这是设计决策而非技术决策。引用链的深度问题。Figma 变量支持多层引用语义 Token 引用中间 Token中间 Token 引用原始 Token。深度引用链在提取时需要递归解析而递归可能产生循环引用如果设计师在 Figma 中不小心把 A 引用了 BB 又引用了 A。提取脚本必须加入循环检测逻辑避免无限递归。多平台输出的复杂性。CSS Variable 只解决了 Web 端的问题iOS 和 Android 各有自己的 Token 体系。JSON 中间格式是跨平台同步的基础但 iOS 的 Swift Color 需要不同的数值格式Android 的 XML Resource 有自己的命名规则。管道需要额外的转换层适配每个平台的语法习惯这些转换层的维护成本随平台数量线性增长。五、总结设计 Token 管道的本质是让设计意图无损传递到代码仓库。从 Figma 提取到 CSS Variable 输出三层管道——提取、映射、输出——各司其职把设计师在画布上的每一次调色变成代码中每一行自定义属性的更新。管道搭建的核心难点不在技术实现而在命名语义的桥梁搭建——从视觉命名到语义命名的映射需要设计决策而非代码逻辑。自动化能加速传递但不能替代设计师对这个蓝色到底是背景色还是链接色的判断。管道搭好之后它就不是一个人跑的赛道而是设计系统的心血管——设计师改一个 Token几分钟内所有使用这个 Token 的组件自动更新。这才是设计系统真正在呼吸的时刻。