跳到正文
深圳 · 大湾区 · 地球

设计工程:消灭 Figma 到生产的交接损耗

交接环节是设计意图被压缩、再由当周值班的人重新解压的地方。设计工程取消了这个中间步骤:token 成为接口,部署预览成为评审面。

8 分钟阅读1,831
Design EngineeringNext.js ArchitectureSystems本文同时提供 English

交接是一次有损压缩

交接是对设计意图的有损压缩,而站在中间的那个人就是解压算法——所以最终能还原到什么程度,取决于那一周恰好是谁在值班。

我在那条流水线的压缩侧待了几年:带 UI 团队,写足够精确、能扛过一轮翻译的规范文档。问题从来不在规范本身。问题在于每一次翻译都经过一个人,而每个人都会优先优化自己那一步的完成度。

设计工程不是“会写代码的设计师”,也不是“在意字距的工程师”。它是一个决定:把翻译这一步彻底删掉。让同一个人从画板一路负责到线上像素,中间的产物是可被机器读取的,而不是散文。

从画板到构建之间,真正丢掉的是什么

意图设计文件里存的实际交付的谁会先发现
垂直节奏画板上的位置,常常是手动微调过的各组件各自的 margin,彼此不构成体系没人发现,直到两个组件叠在一起
字号体系桌面画板里的名义字号一个固定字号,没有流体行为移动端用户,在每个断点上
交互状态偶尔有一个 hover 变体浏览器默认的焦点环键盘用户,无障碍审计
动效点击才播放的原型transition: all 0.3s ease所有人,表现为低端 Android 上的卡顿
空状态与错误状态没有画API 返回什么就是什么,且没有样式客服工单,上线第二周
长内容长度舒适的 Lorem ipsum换行后的德语复合词撑破栅格本地化团队,每季度一次

规律是一致的:画板只编码了单一视口下的理想路径,而生产环境里大部分是九种视口下的非理想路径。交接不会大声失败,它的失败方式是“差不多对”的决策缓慢累积,成本最后体现在评审轮次上,而不是 bug 列表里。

一个中等复杂度的界面——比如一个带十几个控件的设置面板——从规范到实现走完一个来回,跨两个人要花四到十小时,而第二个来回存在的意义,主要是补上第一个来回丢掉的东西。乘以界面数量,就是这套流程真实的价格:不是复合岗位的薪资差,而是每个界面都要被碰两遍的复利成本。

Design token 是设计与代码之间的接口

如果只允许做对一件事,那就是这一件:设计与代码之间的契约应该是一个文件,放在仓库里做版本管理,格式双方都能从它生成各自的产物。

用 W3C Design Tokens Community Group 的格式作为唯一事实来源。它无聊、可移植,任何正经工具都读得懂。

{
  "$schema": "https://design-tokens.org/schema.json",
  "color": {
    "surface": {
      "base": { "$type": "color", "$value": "oklch(0.98 0.003 250)" },
      "raised": { "$type": "color", "$value": "{color.surface.base}" },
      "inverse": { "$type": "color", "$value": "oklch(0.19 0.012 260)" }
    },
    "accent": {
      "default": { "$type": "color", "$value": "oklch(0.62 0.14 250)" },
      "contrastText": { "$type": "color", "$value": "{color.surface.base}" }
    }
  },
  "space": {
    "2": { "$type": "dimension", "$value": "0.5rem" },
    "4": { "$type": "dimension", "$value": "1rem" },
    "6": { "$type": "dimension", "$value": "1.5rem" }
  },
  "radius": {
    "control": { "$type": "dimension", "$value": "6px" },
    "panel": { "$type": "dimension", "$value": "14px" }
  }
}

有两条规则能让它真正生效,而不是沦为仪式。第一,用语义命名,不用外观命名:surface.raised,而不是 gray-100。第二,每个 token 在设计库和代码主题里都存在,并且都由这个文件生成——设计文件通过插件导入 JSON,CSS 在构建时生成。任何一侧都不手工维护另一侧的副本。

命名是最容易被低估的一环。surface.raised 这类名字会迫使设计讨论围绕用途展开,而不是围绕某个具体色值;反过来,一旦有人把 gray-100 写进组件,颜色一变,这个名字就开始说谎,而会说谎的名字比没有名字更难清理。

用 Tailwind CSS v4,代码这一侧就是一小段主题配置,这正是重点:token 文件是输入,主题只是它的一种生成视图。

/* app/theme.css — generated from tokens/design.tokens.json */
@import "tailwindcss";

@theme {
  --color-surface-base: oklch(0.98 0.003 250);
  --color-surface-raised: oklch(0.99 0.002 250);
  --color-surface-inverse: oklch(0.19 0.012 260);
  --color-accent-default: oklch(0.62 0.14 250);

  --spacing-2: 0.5rem;
  --spacing-4: 1rem;
  --spacing-6: 1.5rem;

  --radius-control: 6px;
  --radius-panel: 14px;

  --text-body: clamp(0.95rem, 0.92rem + 0.15vw, 1.0625rem);
  --text-body--line-height: 1.65;
  --text-display: clamp(1.75rem, 1.2rem + 2.4vw, 3rem);
  --text-display--line-height: 1.08;
  --text-display--letter-spacing: -0.022em;
}
token 的分发方式漂移风险每次改动的工作量最先崩掉的是什么
两侧手工维护改两处,一次评审色阶,悄无声息地
从设计工具单向导出插件改一处,再导出一次token 命名,在第一次重命名时
仓库里的 DTCG JSON,双向生成一个 pull request生成器,在 CI 里大声报错

第三行是我在用的方案。如果有人改了 token 文件却没有重新生成 CSS,CI 会在 diff 上直接失败。这种失败很便宜,而且立刻发生;等三周后在设计评审里才发现同类漂移,就不便宜了。

字号与间距是函数,不是数值

固定字号体系是一个只在一个视口成立的决策。用 clamp() 配合按视口单位缩放的模数比例,只需要一行 CSS,就能消掉一整类断点专属的修补。

有两条约束能让流体字号保持诚实。行高必须随字号增大而减小——1.7 的行高在 16px 时显得宽松,在 48px 时就荒谬了,所以要逐级推导,而不是全局设一个值。正文行宽应落在 60 到 75 个字符之间,这个值更适合用 ch 表达而不是像素,因为 ch 能扛住字号变化,像素不能。

字号之间的间距比字号本身更重要。正文相邻级别用 1.25 的比例,展示级用 1.333,得到的体系让设计师可以直接从集合里挑选,而不必在评审中途临时编一个数值。某个值确实缺失时,正确的做法是在提交组件的那同一个 pull request 里把它加进 token 文件;而不是内联一个魔法数字,让半年后的同事看到它却不敢动。

流体字号还有一个附带好处:不必为每个断点维护一套覆盖值。断点一多,覆盖值之间就会互相打架,而 clamp() 把这件事收敛成一个可以推理的公式:最小值、随视口增长的系数、以及一个上限。

把无障碍做成编译期约束

对比度、焦点可见性、动效偏好、点击目标尺寸,这些不是评审话题,而是在任何人看第一眼之前就该跑完的 lint 规则。

  • 正文文字与其实际背景:WCAG 2.2 AA 要求至少 4.5:1;大字号文本和 UI 组件边界为 3:1。
  • 焦点可见:永远不要在没有替代方案的情况下写 outline: none,替代方案对相邻颜色的对比度要达到 3:1。
  • 目标尺寸:指针目标至少 24 × 24 CSS 像素。
  • 动效:尊重 prefers-reduced-motion,做真正的削减,而不是把时长缩短。
# CI: structural and contrast checks on the built output, not on the source
pnpm exec playwright test tests/a11y.spec.ts --project=chromium
pnpm exec axe --exit --tags wcag2a,wcag2aa --include "main" .next/static
pnpm exec lighthouse-ci autorun --collect.staticDistDir=.next --assert.assertions.categories:accessibility=1

自动化工具能抓到真实无障碍缺陷的三分之一到一半——它能发现缺少 label,但发现不了一个令人困惑的 label。绿色通过只意味着可以开始人工评审,从来不是正确性的证据。真正要紧的缺陷都是语义层面的:标题层级是否与视觉层级一致,该用 <a> 的地方是不是写了 <button>,表单错误是被朗读出来的还是只标了红色。

顺序也有讲究:这些检查要跑在有人看之前,而不是之后。评审时间应该花在判断措辞和信息层级上,而不是逐条核对对比度。

这些规则的工程价值在于可检查——把约束写成机器可校验的东西是同一个思路。“让它感觉无障碍”不是一个任何人能稳定满足的约束;“对相邻表面达到 3:1,且在 CI 中校验”是。

性能预算就是设计约束

性能不是一个靠后的优化环节,它是一组约束设计可以长成什么样的数字。一个带三种字体和 600KB 插图的 hero 视频,不是一个“以后还能优化快”的设计,而是一个已经把预算花完的设计。

指标预算什么会超标能解决它的设计决策
LCP4G 下低于 2.0s未经压缩直接投放的 hero 图预留容器尺寸;AVIF,小于 120KB
INP低于 200ms低端手机上的滚动联动动画只对 transform 和 opacity 做动画
CLS低于 0.02迟到的字体和同意横幅尺寸校正的 fallback 字体指标;为横幅预留空间
路由 JS文章路由 gzip 后低于 170KB整包引入的组件库只打包页面真正用到的那四个组件
字体两个子集,每个低于 40KBlatin+CJK 展示字体的四个字重可变字体、做子集、font-display: swap

这段对话最有产出的时机,是画板还开着的时候。说“那张插图必须移出 LCP 路径”,比等客户确认了视觉稿之后再删掉它,要容易得多。

预算的意义不是卡住设计,而是把取舍提前到还有选择余地的时候。等到交互和视觉都已定稿,剩下的通常只有两条路:要么一直拖着不优化,要么用一次返工去换回几毫秒。

实践中,用什么替代交接

三条协作约定承担了大部分价值。

设计师在部署预览上评审,而不是在静态画板上。一个真机上的预览链接能发现的问题比标注稿更多,而它的成本是一次 push,不是一场会。

token 文件是引入新视觉值的唯一入口,其他一切都引用它。这是一条一句话的规则,却消掉了大部分关于数值的评审争论。

画交互的人来实现它,或者与实现它的人坐在同一个 pull request 里。不是因为协作本身高尚,而是因为保真度的损失发生在边界上,而修复边界最便宜的方式,就是取消这个边界——这也是我为企业团队交付设计工程时的起点。

结语:更少的中间产物,更高的保真度

设计工程不是一个复合岗位头衔,也不是要取消设计评审。它是对一个事实的承认:意图与生产之间的每一份产物,都是意图发生衰减的地方;而保护保真度最便宜的方式,就是让产物的数量变少。token 取代规范文档,部署预览取代标注画板,CI 取代“这个对比度到底能不能过”的争论。剩下的工作,才是原本就属于真正工作的那一部分:决定界面应该是什么样子,然后成为那个让它精确地长成这样的人。

继续阅读

更多「设计工程」

准备构建一套系统?[ 预约会议 ]