作为一个不断探索 AI 和设计工程化实践的设计师,的最终目标,是要在设计与工程之间搭一套真正的 Design Harness(设计控制基座与测试沙箱)。

要让这套 Harness 能够精准约束代码、承载高密度的 AI 交互推演,第一步必须有一套底层完全受控、视觉与工程规则严密闭环的组件库作为基石。现成开源库的审美预设和黑盒逻辑,根本无法作为 Harness 的稳定锚点。

为了打好这第一块地基,我前后折腾了三次。直到第三次踩准人机分工的边界、重构了底座与反馈机制,整套链路才算真正走通。这三次尝试里的思维演进、人机分工的边界,以及极其具体的技术踩坑,才是我最想复盘的经验。

先拿现成的改

第一次尝试的时候,我的想法非常直接,直接从 GitHub 上拉了 shadcn/ui 的源码,在本地起了一个 React 和 Vite 的项目。当时我采取的是纯 vibe 的方式,也就是自己不写详细规格、让 AI 直接做。这样做了大约一天后,我选择放弃。

让我想放弃的原因,主要是从一开始就遇到了几处阻碍。它自带一套已经设计好的视觉规范,这就意味着只要我想按自己的来,就得在某处覆盖一次,覆盖与维护的负担一直在。

与此同时,我想要的那套全局尺寸与间距的语义联动,它的默认体系没有直接满足——比如我想要的是一批同为 32px 高的控件共用一套内边距之类的规则,改动一处,这一批就能一起变,而它的默认体系并没有直接给我这种联动。

平心而论,它不是不能改,它有基于 CSS 变量的主题系统、允许扩展自己的 token(也就是把颜色和圆角这类视觉决定存成有名字的变量),源码本来就开放给我改;只是这次目标是从底层建立自己的视觉规则,而它的默认体系把一部分审美决定做好了,跟我要的联动不是一回事。

再加上它给组件准备的那套 API,对我自己那套组件开发规则和约束来说显得多余,改动起来处处受制。

改一层,还有一层:现成视觉规范带来的覆盖成本。

这一轮没走下去,既有工具默认没能给我这套联动的原因,也有我自己还没先把设计规则系统化的原因。所以第二次,我决定先把设计规则整理成 AI 能直接读取的东西。

回到 Figma

到了第二次尝试,我还是用 shadcn,但把整个工作的先后顺序倒了过来。我先在 Figma 里面把 styles、变量和基础组件定下来,然后再通过 Figma MCP——一个让 AI 直接读取我 Figma 文件里那套定义的通道——转出去并在代码层面进行延伸。

颜色我来定,批量工作交给 AI

用 Figma 把全局变量和视觉样式定下来,比一句句口头让 AI 猜更直接、更快。因为想要的颜色、圆角、间距先落成了文件里能指认的定义,AI 照着读就好,不用我一遍遍在对话里重新描述。分工在这一轮里变得很清晰:颜色和梯度是我自己定的,每一档红、每一档灰、什么时候用哪个,完全是我的设计判断;

AI 负责的是批量处理——把颜色变成变量、把圆角和模糊这类数值变成变量、把设计转成代码。这种分工不是二选一,而且当 AI 在批量转变量、写代码的时候,我可以切回画板继续推演下一组组件的设计,不需要停下来等它。我定什么是对的,它负责写全、写一致。

按视觉结构,把组件归成几类

在这一轮,我对组件的划分逻辑进行了一次彻底解构。我放弃了传统文档命名,完全按「视觉骨架长得像什么」来归纳:

  • 按钮族:涵盖 Button、Tag、IconButton、SplitButton、Chip。它们本质上都是一个背景容器里塞着“图标—文字—图标”。视觉排布逻辑高度共享,但 HTML 语义和行为必须各自分开。

  • 输入族:涵盖 Input、Textarea、Select、Password、OTP。容器、边框和状态激活反馈逻辑同源,但内部交互逻辑完全不同。

  • 浮层与信息反馈:Modal、Dialog、Confirm 归入弹窗族;Menu、Popup、Popconfirm 归入气泡族;Alert、Toast、Message 归入提示族。

这套分类沉淀下来的设计资产非常扎实,目前依然在我的项目里稳定运行。但我依然被卡住了:自己手搭的 React + Vite 预览环境成了新的包袱。我发现自己一边在做设计规则,一边还要花大量精力去修补那个用来看组件的临时页面,工作重心被严重稀释。

我归纳的是它们的视觉结构,至于怎么出现、何时关闭,要按具体组件分别定义。最具体的类比是按钮和标签:它们视觉上很近,都是一个背景容器里按「图标—文字—图标」排,这部分视觉可以共享;但行为与 HTML 语义仍要分别定义,视觉相近不等于可以合并成一个东西;

同族的下拉也不是把输入框的标签名换一个就完事。这套分类是我长期做组件库攒下来的个人方法,不是什么行业标准;视觉能复用不代表行为与语义能复用。

按钮和标签可以共享视觉结构,行为与语义仍要分别定义。

第二次尝试沉淀下来的设计基础是有效的,到现在我的项目里还在用;但我对整体依然不满意。封装的冗余还在,我自己搭的那个 React + Vite 预览环境也要我一直维护,我等于同时在维护一套设计和一套用来看设计的工具。

换底座,把判断写成规则

做第三次尝试之前,我和一个朋友聊了聊,他建议我看看 Storybook。我自己也想到了 Base UI 这个无头的 React 组件库。我不是从零开始,而是踩着第二次的设计成果换了底座,只用了一天就完成了当时规划的组件库开发,达到了我个人的视觉满意,这一次是成功的。

让底座管行为,把样式留给自己

首先是 Base UI 管住了行为和结构。“无头”,也就是 headless,可以理解为底座提供组件行为与可组合的结构,样式由我来定义。不过无头并不等于没有 API,不等于组件全都齐,也不等于它自动满足我的设计需求。

给颜色起一个能说明用途的名字

在样式层面,语义变量管住了视觉一致。所谓语义变量,就是按用途命名而不是按颜色命名,文字、边框、背景、交互、表面这些具体用途被赋予了明确的名字。在命名规则里,negative 指的是负向或危险动作那一类,intense 指的是强调用的较深背景那一档,hover 则指代鼠标悬停状态。

这是从我项目 token 文件里选的一小段,不是完整样式表。

:root {
  --nico-color-text: var(--nico-color-grey-alpha-1300);
  --nico-color-text-negative: var(--nico-color-red-500);
  --nico-color-border: var(--nico-color-grey-alpha-400);
  --nico-color-border-negative: var(--nico-color-red-500);
  --nico-color-background-input: var(--nico-color-grey-alpha-300);
  --nico-color-background-negative-intense: var(--nico-color-red-500);
  --nico-color-background-negative-intense-hover: var(--nico-color-red-600);
  --nico-color-interaction-hover: var(--nico-color-grey-alpha-300);
  --nico-color-interaction-hover-negative: rgb(from var(--nico-color-background-negative-intense) r g b / 0.04);
  --nico-color-surface: var(--nico-color-grey-opaque-100);
}

这段代码的组织逻辑可以通俗地分为三层:底层是一层层色阶,比如 red-500、grey-alpha-400;中间是按用途起的语义名字,涵盖 text、border、background、interaction、surface;具体的组件引用这些语义名字,而不是直接引用色阶。

落到一句话:一个危险动作要用的文字色、边框色、背景色、悬停色,我都能按名字点出来。

比如按钮的 negative plain 与 ghost 用 interaction-hover-negative,negative filled 用 background-negative-intense 和它的 hover。

正因为有这些能指着说的名字,后续要调整的时候,例如可以对 AI 说「把 negative 的 intense 悬停再深一档」,而不必重新描述一遍颜色。有了明确的变量层,我在 Storybook 里看到不满意才能给出具体的修改要求;

但这只是建立了清晰的引用规则,并不代表所有组件都自动遵守语义变量。

在 Storybook 里看,再让 AI 改

到了界面调试环节,Storybook 替我管好了反馈回路。它让我能单独把一个组件的一个状态摆出来看,还能带文档和测试。我的回路变成:肉眼看,不满意,给 AI 一个具体的修改要求,再看。

Storybook 不是 React 或 Vite 的替代品,我换掉的是自己搭的展示工作台,项目里现在仍然是标准的 React + Vite 集成。

这些能够凑效,前提是我手里已经有第二次定下来的 styles、变量与基础组件,换底座时这部分直接继承,没有重做。我现在的项目依赖 Base UI,开发和构建入口用的是 Storybook 的 react-vite 集成。

看见状态差异,给出具体修改,再看一次。

在整个摸索过程中,我也踩过不少具体的坑。

一些注意事项

一类坑在于说法不够具体。比如 duration(时长),我让 AI 做 duration 的时候它总不理解,我要的其实是一个起止区间,说成 start 和 end 才对得上;当时这个我没做出来。

需要区分的是,现在项目里已经有这个名字下的 start/end 实现了,所以不是永远做不出来,而是当时那个说法没能让它对上我的需求。又比如 readonly 和 disabled,它们代表只读与禁用两种状态且并不等价;光靠属性名字不够,得写清楚能不能聚焦、能不能打开面板、能不能改值。

这本质上是我们之间没把行为约定对齐,并不是库不理解人话,也不能把两个属性说成等价。例如,如果我希望这两种状态有下面的区别,就可以明确告诉 AI:「只读状态下可以聚焦但不能唤起选择面板,禁用状态下不可聚焦且不响应交互」,把约定讲透。

另一类坑则是那些需要自己二次开发的需求。我预期的时间精度和组合控件仍然要自己二开,我现在自己写的时间工具能支持到分钟、秒和毫秒,这是项目里自建的组件,不是 Base UI 原生自带的;这一类坑不是把话说得更清楚就能解决,它确实需要写代码。

一类先澄清约定再检查实现,另一类明确要二次开发。把规则说清楚、把设计资产握在手里、选用把样式交还给我的底座,再加上能看清单个状态的反馈回路,这四件事凑齐,让我在当时规划的范围内做到了视觉满意,超出那个范围的部分仍要二开。真正省下来的是我维护「看设计的工具」的精力,以及一遍遍解释视觉的成本。

总结

搭建 Design Harness 的第一步,本质上是在解决“控制权”“解释成本”的问题:如果底层组件带着别人的审美黑盒,后续的 Harness 就无法对 UI 状态实现完全受控的监测与干预;如果自己的设计语言没有抽象成精准的 Token 契约,AI 在工程链路里的执行就会陷入无休止的模糊拉扯。

对于初入设计工程实践的设计师朋友,把视觉落地到代码里,关键在于怎样把自己的视觉判断变成有名字、能指认的规则,并在一个干净的环境里真正看清组件的每一次细微反馈。不要急着在代码里去覆盖别人预设好的审美,先在设计端建立起属于自己的语义约束,再交给合适的无头底座去承载结构。

把设计规则抽象成有名字、可指认的 Token;选一个剥离了审美包袱的无头底座(Base UI);用专业的切片沙箱(Storybook)建立反馈回路。做好了这三点,这套组件基石才算真正立住,往后搭建完整的 Design Harness 才会拥有稳固的抓手。

当语言和规则对齐了,代码和工具才能真正顺着你的直觉跑起来。