如果你刚学会任意值,做一张品牌资料卡可能会写成这样:
<article class="rounded-[18px] border border-[#DDE7E2] bg-[#F7FBF8] p-[22px] shadow-[0_14px_40px_rgba(19,78,57,0.12)]">
<p class="text-[12px] font-semibold tracking-[0.16em] text-[#287A5B]">春山工作室</p>
<h2 class="mt-[10px] text-[26px] font-bold text-[#153C2E]">让复杂信息更容易被看懂</h2>
<p class="mt-[8px] text-[15px] leading-[1.7] text-[#567064]">品牌资料卡正文</p>
<button class="mt-[18px] rounded-[10px] bg-[#1F6B4F] px-[18px] py-[10px] text-[#FFFFFF] hover:bg-[#18533E]">
查看案例
</button>
</article>这段代码没有语法错误,画面甚至可能挺好看。麻烦会在第二张、第三张卡片出现:有人把主绿色抄成 #206C50,有人觉得按钮圆角 12px 更顺眼,还有人复制阴影时少抄了一段。一个月以后,页面里会长出十几种“差不多的绿”和一排肉眼难以分辨的圆角。
我知道你可能会想:把这些值直接写在 class 里,不正是 Tailwind 允许的吗?确实允许,但“允许临时越界”和“鼓励把所有决定散落在标签里”是两回事。任意值解决的是例外,主题系统解决的是共识。这一节我们要做的,就是把品牌色、字体、圆角、阴影和断点从一次性的数值,整理成团队能反复使用的设计语言。
改造完成后,同一张资料卡会更像这样:
<article class="rounded-brand border border-brand-line bg-brand-canvas p-card shadow-brand-card">
<p class="text-brand-kicker font-semibold tracking-brand-label text-brand-accent">春山工作室</p>
<h2 class="mt-2.5 text-brand-title font-brand font-bold text-brand-ink">让复杂信息更容易被看懂</h2>
<p class="mt-2 text-base leading-relaxed text-brand-muted">品牌资料卡正文</p>
<button class="mt-4.5 rounded-control bg-brand-action px-control-x py-control-y font-semibold text-white hover:bg-brand-action-hover">
查看案例
</
类名没有明显变少,但每一个值都有了角色。brand-action 不是“某个绿色”,而是“主要操作使用的颜色”;brand-card 不是一串阴影参数,而是“卡片的抬升层级”。以后品牌换色,改定义就够了,不必在几十个组件里搜十六进制值。
主题不是一份为了显得规范而存在的色值清单。只有当名字能稳定表达团队的设计决定,并且多个界面愿意复用它时,它才算设计 token。一次性的尺寸硬塞进主题,只会把杂乱从 HTML 搬到 CSS。
配置之前,先别急着起 primary、secondary、color-1 这类名字。我们要回答的是:这个值在界面里负责什么?拿品牌资料卡来说,最少会遇到下面几类决定。
先把这张整理过程放在脑子里:左边的值都能用,但彼此没有关系;右边没有凭空增加更多样式,只是把反复出现的决定收进了几把公共尺子。

看图时别只盯着“整齐”两个字。真正的变化是团队以后调整颜色、间距或圆角时,知道该打开哪个抽屉,也知道哪些组件应该跟着一起变。
这里有一个实用判断:颜色原料和语义角色可以同时存在,但不要混为一层。pine-700 适合表示一档稳定的色板原料,brand-action 适合表示它在当前产品中的职责。如果以后主要按钮从松绿色改成砖红色,bg-brand-action 仍然说得通;bg-pine-700 虽然没有失效,却把旧视觉事实写进了组件。
小项目不一定要把两层都建齐。只有一个页面时,brand-action、brand-canvas 这类角色名已经够用。多品牌产品、设计系统包或需要做主题切换的产品,再把“原料层”和“语义层”拆开,会更容易治理。
先圈出重复出现的决定。两张卡片都使用同一套主色、圆角和阴影,这些值有资格进入主题;某张海报为了对齐插画而偏移 13px,暂时不需要升级成 token。
再按职责命名。问自己“它为什么存在”,比问“它看起来是什么颜色”更可靠。主要操作叫 action,危险操作叫 danger,页面底色叫 canvas。
最后检查名字能否跨组件成立。profile-card-green 只能服务一张资料卡,brand-action 可以同时服务按钮、链接和选中状态,后者更像设计语言。
@theme 不只是换一种写 CSS 变量的方法Tailwind CSS v4 把主题配置放进 CSS。最小入口通常从一条导入和一个顶层 @theme 开始:
@import "tailwindcss";
@theme {
--color-brand-action: #1f6b4f;
}定义 --color-brand-action 后,Tailwind 能识别 bg-brand-action、text-brand-action、border-brand-action 等颜色工具类。编译后的样式中还会得到普通的 --color-brand-action CSS 变量,因此普通 CSS、内联样式或动画代码也能使用 var(--color-brand-action)。
这正是 @theme 和 :root 的分工:
:root {
--map-panel-height: 23rem;
}
@theme {
--color-brand-action: #1f6b4f;
}--map-panel-height 只是应用内部变量,不需要生成一套 Tailwind 类名,放在 :root 很合适。--color-brand-action 则同时声明了一个主题决定和相应的工具类 API,所以要放进 @theme。@theme 必须写在顶层,不能塞进 .dark、媒体查询或组件选择器里。
@theme 不会根据英文单词猜用途,它看的是变量前缀。常用命名空间可以先记住这一组:
同一个变量经常不只对应一个类。颜色 token 能进入背景、文字、边框等多套颜色工具类;断点变量不生成一个叫 breakpoint-wide 的类,而是生成 wide: 变体。理解命名空间后,你就不用背“配置字段到工具类”的两套词典了。
如果这张表看起来还有点抽象,可以把命名空间想成生成机器的入口:变量从哪个抽屉送进去,决定另一端会出来哪一类工具能力。

试着沿图追一遍:颜色可以流向背景、文字和边框,断点则不会变成视觉属性,而会成为控制这些属性何时生效的条件。这样再读变量前缀,就不容易把“值”和“变体”混成一件事。
下面把资料卡需要的颜色、字体、字号、间距、断点、圆角和阴影一次放好:
@import "tailwindcss";
@theme {
--color-brand-canvas: #f7fbf8;
--color-brand-ink: #153c2e;
--color-brand-muted: #567064;
--color-brand-line: #dde7e2;
--color-brand-accent: #287a5b;
--color-brand-action: #1f6b4f;
--color-brand-action-hover: #18533e;
--font-brand: "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
--text-brand-kicker: 0.75rem;
--text-brand-title: 1.625rem;
字体文件的加载规则不属于 @theme 的职责。--font-brand 只说明字体栈;如果项目使用自行托管的字体,还要在 CSS 中另写 @font-face,或者由应用的字体加载方案完成下载与预加载。
现在看类名与结果的对应关系:
<section class="grid gap-6 wide:grid-cols-3">
<article class="rounded-brand border border-brand-line bg-brand-canvas p-card shadow-brand-card">
<p class="text-brand-kicker font-semibold tracking-brand-label text-brand-accent">设计与开发</p>
<h2 class="mt-2.5 font-brand text-brand-title font-bold text-brand-ink">春山工作室</h2>
<p class="mt-2 leading-relaxed text-brand-muted">把产品信息整理成清楚、可靠的界面。</p>
</article>
</在普通宽度下,这里是一列网格;视口达到 90rem 后变成三列。卡片的背景、边框、圆角和阴影都由主题供给。如果设计师把卡片圆角从 18px 调到 20px,只改 --radius-brand,所有 rounded-brand 会一起更新。
下面这张图把“布局会变,品牌不变”拆成三个视口。留意卡片列数怎样随断点变化,同时比较主色、操作色和圆角有没有被每个尺寸重新定义。

这就是把断点也纳入主题的意义:组件负责在某个条件下换布局,品牌 token 继续负责它看起来属于谁,两套职责不会因为屏幕变化而搅在一起。
现在可以到调校台里亲手改一次 token。先保持预览组件的类名不动,再调整颜色、间距和圆角,观察多个使用点是不是会同步响应。
操作完后,再回头检查预览中的 bg-brand-500、p-6 和 rounded-brand:类名表达的角色没有变化,变化的是主题给这些角色提供的值。这正是集中管理与直接复制数值之间最明显的区别。
主题变量的名字是项目 API。把 --radius-brand 改成 --radius-soft-card,不只是改 CSS 变量名,也会让 rounded-brand 这个类消失并出现 rounded-soft-card。对已有项目做这类改名时,要像调整函数参数一样检查使用方。
旧稿最容易把这三种动作写混。事实上,单独增加一个新变量、重定义同名变量、清空整个命名空间,影响范围完全不同。
@theme {
--color-brand-action: #1f6b4f;
--radius-brand: 1.125rem;
}这是日常最稳妥的做法。默认的 bg-blue-500、rounded-lg 仍然可用,同时多出 bg-brand-action 和 rounded-brand。适合逐步引入品牌层,不要求团队当天就重写全部页面。
@theme {
--breakpoint-sm: 30rem;
--radius-lg: 0.875rem;
}sm: 和 rounded-lg 仍然存在,但含义已经变了。覆盖的风险不是“类消失”,而是所有旧使用点会一起改变。断点尤其要谨慎:把 sm 调小,可能让导航、表格和表单同时提前切换布局。动手前先盘点影响面。
扩展和覆盖可以用两张工作台来对照。左边只是添了一个项目抽屉,右边则动了原有抽屉;后者看起来同样是“改几个值”,影响范围却完全不同。

观察中间的警示标记:覆盖不是不能做,而是要先确认旧组件是否依赖原来的含义。这个检查动作,正是扩展通常比覆盖更适合渐进迁移的原因。
下面的映射室把这种影响变成了可操作对比。先切换不同命名空间,看变量会生成哪些工具类或变体;再在“扩展”和“清空后重建”之间来回切换,记录哪些默认能力仍然存在。
做完以后,重点核对两件事:新增项目颜色时默认色板有没有保留,以及清空颜色命名空间后旧类名是否还会产生样式。只要这两个结果分得清,扩展、覆盖与清空就不会再混用。
@theme {
--color-*: initial;
--color-white: #ffffff;
--color-brand-canvas: #f7fbf8;
--color-brand-ink: #153c2e;
--color-brand-action: #1f6b4f;
--color-brand-danger: #b42318;
}这时默认色板对应的颜色类不再可用,只有你重新定义的颜色词汇留下。也就是说,bg-blue-500 会消失,bg-brand-action 可以使用。若要连字体、阴影、断点等全部默认主题一起放弃,可以使用 --*: initial,然后从零补回需要的变量。
彻底清空适合有成熟设计系统、愿意维护完整 token 清单的团队。教学项目或仍在探索视觉方向的产品,贸然清空通常只会让开发者不断补洞。
保留 Tailwind 默认主题,新增品牌角色。优点是迁移平滑,缺点是开发者仍可能绕过品牌规范,继续使用默认色。
@theme 需要位于顶层,那暗色模式怎么改值?答案不是把 @theme 写进 [data-theme="dark"],而是用普通 CSS 变量保存会在运行时变化的值,再让主题工具类接到这些变量上。
:root {
--app-canvas: #f7fbf8;
--app-ink: #153c2e;
--app-muted: #567064;
--app-line: #dde7e2;
--app-accent: #287a5b;
--app-action: #1f6b4f;
--app-action-hover: #18533e;
}
[data-theme="night"] {
--app-canvas: #10231c;
页面依然只写 bg-brand-canvas text-brand-ink。切换根节点的 data-theme 后,普通变量改变,组件随之换色。组件不需要知道当前是亮色、暗色还是另一个品牌。
这里的 inline 不是“让 CSS 写在 HTML 里”。它要求生成的工具类直接使用右侧值。以 --color-brand-canvas: var(--app-canvas) 为例,工具类会直接落到 var(--app-canvas),避免变量在不同 DOM 层级解析时拿到意外值。凡是主题变量的值依赖另一个 CSS 变量,尤其那个变量可能被子树覆盖时,优先考虑 @theme inline。
<main data-theme="night" class="bg-brand-canvas p-8 text-brand-ink">
<button class="rounded-control bg-brand-action px-control-x py-control-y font-semibold text-brand-canvas hover:bg-brand-action-hover">
保存资料
</button>
</main>结果是:同一套按钮结构不变,只由运行时变量决定色彩。注意 text-brand-canvas 在这个例子里借用了页面底色作为按钮文字色,真实项目如果对比度要求更复杂,最好单独设 --color-brand-on-action,不要假设任意主题下“底色一定适合当按钮文字色”。
假设侧栏宽度由用户拖动,值可能是 287px、314px 或 365px。它不是有限设计刻度,没必要为每个宽度生成 token。可以让 JavaScript 只更新普通变量,再通过一次任意值使用它:
<aside style="--panel-width: 314px" class="w-(--panel-width) min-w-64 max-w-xl">
可拖动侧栏
</aside>这里主题继续负责 min-w-64、max-w-xl 这样的边界规范,运行时变量负责当前状态。Tailwind 不需要接管所有动态计算;把稳定规则和实时状态分开,代码反而更清楚。
品牌主题切换之后仍要检查文字与背景的对比度、焦点状态和禁用状态。把颜色变成变量只解决“怎么换”,不会自动保证“换完仍然可读”。设计系统治理里,这类视觉验收不能省。
static 解决的是“没被扫描到也要输出”Tailwind 默认按实际使用情况生成主题相关内容。多数应用只消费自己写到模板里的类,这样很合理。但设计 token 有时要提供给扫描范围以外的地方,例如独立图表脚本、运行时插件,或者一个只暴露 CSS 变量的共享包。
@theme static {
--color-chart-positive: #2e8b67;
--color-chart-warning: #c47a16;
--color-chart-negative: #b42318;
}static 要表达的是:即使当前模板没有出现 text-chart-positive 之类的类,也始终输出这些主题变量。它不是“禁止主题变化”,也不是“把值写死在每个工具类里”。
inline 和 static 名字都像是在改变“生成方式”,但两者回答的问题不同。下面四格图把解析时机与输出范围拆开:上排看变量值怎样落进工具类,下排看哪些主题资源会被生成。

看图时可以各问一句:inline 解决“工具类最终引用谁”,static 解决“没有扫描到使用点时还要不要输出”。一个管解析,一个管范围,不是互相替代的开关。
普通页面不要无脑给所有 @theme 都加 static。默认行为能减少没有使用的输出;只有当 CSS 变量本身就是交付接口,或者消费方无法被当前构建过程看见时,固定输出才有价值。
JavaScript 需要读取主题值时,也不必再维护一份重复对象:
const styles = getComputedStyle(document.documentElement)
const positiveColor = styles.getPropertyValue("--color-chart-positive").trim()这段代码得到浏览器最终解析后的值,图表和 DOM 样式共用同一套 token。若脚本无论当前页面是否出现相关工具类都要读取这个变量,就应把对应变量放进 @theme static。
CSS-first 配置带来的一个直接好处,是共享主题不需要先包装成 JavaScript 预设。可以把稳定 token 放进独立文件:
/* packages/brand/theme.css */
@theme {
--color-brand-canvas: #f7fbf8;
--color-brand-ink: #153c2e;
--color-brand-action: #1f6b4f;
--radius-brand: 1.125rem;
--shadow-brand-card: 0 0.875rem 2.5rem rgb(19 78 57 / 0.12);
}各应用在自己的入口 CSS 中导入 Tailwind 和共享主题:
/* apps/admin/app.css */
@import "tailwindcss";
@import "../../packages/brand/theme.css";
@theme {
--color-admin-selection: #dcefe5;
}管理后台拿到品牌通用词汇,同时保留本应用专属 token。营销站也能导入同一个 theme.css,再增加自己的展示字号。这样做的关键不是文件能被复用,而是职责要清楚:共享文件只收跨产品稳定的决定;某个页面的临时值留在应用内。
共享主题最好配一份变更约定:哪些变量可以删除,哪些改值会造成大面积视觉变化,废弃名要保留多久。CSS 能让分发变简单,但不会替团队自动完成版本治理。
把这套结构画出来,会更容易看清“共享”的边界:中央文件只放稳定决定,三个消费方仍然可以在各自入口补充局部 token。

顺着三条连线检查一次:如果某个值只服务管理后台,它就不该因为“以后也许能复用”而提前塞进中央手册;真正跨项目稳定的颜色、字体和圆角,才适合一处维护、多处同步。
方括号语法很好用,而且该用时就用。背景装饰必须精确偏移 117px,可以写:
<div class="top-[117px] lg:top-[344px]">装饰图层</div>复杂网格只在一个数据页出现,可以写:
<section class="grid grid-cols-[15rem_2rem_minmax(0,1fr)]">
数据筛选与结果区
</section>值里需要空格时用下划线占位,编译时会还原为空格。若 Tailwind 没有某个属性对应的工具类,还能直接写任意属性:
<div class="[mask-type:luminance] hover:[mask-type:alpha]">
带遮罩模式切换的图层
</div>它们比 style 多一个明显优势:仍然可以组合 hover:、lg: 等变体。但这不意味着所有视觉数值都该留在方括号里。
我们可以用三问决定是否把任意值升级为 token:
如果三问中有两项答“是”,通常值得进入主题。于是资料卡可以经历这样的演变:
这张图把三问压缩成了一条路线。一次例外可以离开共同刻度,但当同类例外反复出现,继续让它们各走各的,修改成本就会开始累积。

观察逃生舱重新接回主路线的位置:升级为 token 的触发点不是“方括号看着不舒服”,而是这个值已经有了复用关系和共同变更的理由。
<!-- 探索期:先验证画面 -->
<article class="rounded-[18px] shadow-[0_14px_40px_rgb(19_78_57_/_0.12)]">
...
</article>
<!-- 复用期:把共识提升成 token -->
<article class="rounded-brand shadow-brand-card">
...
</article>第一种写法不是罪证,它记录了探索。问题出在方案稳定以后仍然复制任意值。逃生舱的门可以开,但团队不能天天住在里面。
另一个常见误区是为了“消灭方括号”而制造 --spacing-117、--color-one-off-banner。这只是把偶发值伪装成系统词汇。主题规模越大,选择成本越高;没有复用关系的值,留在使用点反而诚实。
@utility 用来补能力,不用来藏回整套组件主题定义的是值,@utility 补充的是项目需要、Tailwind 又没有提供的原子能力。比如品牌字体缺少某档粗体时,我们不希望浏览器擅自合成一个看似发糊的假粗体:
@utility no-synthetic-type {
font-synthesis: none;
}定义后可以直接和字体、字号工具类组合:
<h2 class="font-brand text-brand-title font-bold no-synthetic-type text-brand-ink">
春山工作室
</h2>如果字体包没有加载真正的粗体字形,浏览器不会临时伪造粗体,问题会直接暴露出来,团队也更容易补齐字体资源。这个自定义类只负责一件事,很容易预测,不会悄悄改字号、颜色或间距。
如果某个能力需要读主题 token,也可以直接使用生成的 CSS 变量:
@utility brand-focus-ring {
outline: 0.1875rem solid color-mix(in oklab, var(--color-brand-action) 55%, transparent);
outline-offset: 0.1875rem;
}<button class="rounded-control bg-brand-action px-control-x py-control-y text-white focus-visible:brand-focus-ring">
保存资料
</button>结果是键盘焦点出现一圈与品牌操作色协调的轮廓,鼠标普通点击不会一直显示。focus-visible: 仍然可以作用在自定义 utility 上。
下面这张图把可组合性画成“原子能力盖章”:每枚印章只负责一件事,同一能力可以落到不同组件上,也可以继续接受状态和响应式变体的约束。

沿着按钮、卡片和徽章看一遍,你会发现“复用能力”和“封装组件”并不是同一层:印章提供单项 CSS 能力,组件仍然决定结构、内容和状态组合。
接着到债务门诊里处理几个真实类名。先判断哪些方括号值确实只是一次例外,再把重复的颜色、圆角和阴影提炼成 token,最后逐项验证自定义工具在悬停、按下和键盘焦点状态下是否仍能组合。
完成后不要只看“消灭了多少个方括号”。更有用的复盘是:每个被提炼的值是否真的有共同职责,以及 @utility 是否仍然只封装一项可预测的能力。答不上来,就先保留局部写法。
但不要借 @utility 把所有组件重新塞回一个 .btn-primary:
/* 不建议:一个 utility 同时藏了结构、颜色、状态和动画 */
@utility btn-primary {
padding: 0.625rem 1.125rem;
border-radius: 0.625rem;
background: #1f6b4f;
color: white;
font-weight: 600;
transition: background-color 160ms ease;
}如果这是一个真正的产品按钮组件,它还会有尺寸、图标、加载、禁用和危险操作等状态,应该由框架组件封装结构和行为,主题负责值。@utility 更适合补一个可组合的 CSS 能力,而不是把 utility-first 又改回“猜类名背后藏了什么”。
已有 v3 项目不需要在一次提交里把所有配置推倒重来。v4 仍能加载旧 JavaScript 配置,但不会自动发现它,需要在 CSS 中明确写出:
@import "tailwindcss";
@config "../../tailwind.config.js";
@theme {
--color-brand-action: #1f6b4f;
}这种状态适合渐进迁移:旧配置继续工作,新 token 先用 CSS 管理;能够合并时两边会合并,冲突处以 CSS 侧定义优先。不过它是兼容通道,不代表 v3 配置的每个选项都原样保留。旧配置中的 corePlugins、safelist、separator 在 v4 兼容模式里不受支持,类名保留应转向 v4 的扫描与 source 机制。
以前有人在 JavaScript 中调用配置解析函数,把主题变成对象交给图表库。v4 更适合让 CSS 变量成为交接面:DOM 样式直接用 var(--color-brand-action),脚本确实需要最终值时用 getComputedStyle 读取。这样不会在 CSS 和 JavaScript 各维护一份色板。
我建议按下面的顺序迁移:
先列出现有配置里真正属于设计 token 的颜色、字体、圆角、阴影和断点,不要把插件逻辑与 token 混在一起搬。
把高频、低争议的 token 移入 @theme,保持旧类名含义不变。此时目标是建立单一事实,不是顺便重做视觉设计。
再处理需要运行时变化的语义颜色,用普通变量配合 @theme inline。迁移前后分别检查亮色、暗色和嵌套主题区域。
最后清理旧配置与不受支持的选项。只有确认所有消费方都已迁走,才删除兼容入口。
不要把“升级 Tailwind”和“重命名整套设计 token”绑成一次大改。前者改变配置载体,后者改变项目 API,同时进行会让视觉回归很难定位。
不会。重定义 --color-blue-500 只会改变这个变量。要清掉一整个颜色命名空间,需要明确写 --color-*: initial。这两种动作不要混讲。
@theme”也不对。只有希望参与 Tailwind 工具类或变体 API 的变量才放 @theme。弹窗当前坐标、拖拽宽度、图表临时计算结果放普通变量更自然。
token 只能约束可选词汇,不能替团队做选择。如果同一层级有人用 shadow-brand-card,有人用 shadow-2xl,视觉仍然会漂移。需要配合组件边界、设计评审和废弃策略。
任意值数量只是线索,不是成绩。一个仅出现一次的插画偏移保留 top-[117px] 很合理;为了它创造一个永久 token,维护成本反而更高。
主题擅长统一颜色、字体、间距、圆角、阴影和断点。极度定制的品牌视觉可能包含不规则遮罩、复杂排版规则、艺术字和按内容计算的布局,这些仍然需要普通 CSS、SVG 或组件逻辑。Tailwind 是设计系统的接口之一,不是 CSS 能力的上限。
最后把本节思路合在一起。主题先声明稳定的视觉词汇:
@import "tailwindcss";
:root {
--app-canvas: #f7fbf8;
--app-ink: #153c2e;
--app-muted: #567064;
--app-line: #dde7e2;
--app-accent: #287a5b;
--app-action: #1f6b4f;
--app-action-hover: #18533e;
}
[data-theme="night"] {
组件只消费这些词汇:
<article class="rounded-brand border border-brand-line bg-brand-canvas p-card font-brand text-brand-ink shadow-brand-card">
<p class="text-xs font-semibold tracking-[0.16em] text-brand-accent">春山工作室</p>
<h2 class="mt-2.5 text-2xl font-bold">让复杂信息更容易被看懂</h2>
<p class="mt-2 leading-relaxed text-brand-muted">品牌资料、核心服务和下一步操作被收在同一张卡片里。</p>
<div class="mt-5 flex flex-wrap gap-3">
<button class=
最终效果并不神秘:一张有统一圆角和阴影的资料卡,主按钮和次按钮层级明确,切换主题后核心颜色跟着变化。真正有价值的是,界面变化不再依赖大家记住同一串十六进制值,而是依赖一组有边界、能讨论、能版本化的名字。
设计系统最常见的失败,不是不会写 @theme,而是什么都往里面放。最初只有十几个变量,半年后出现 --color-green-new、--color-green-final、--color-card-special,开发者不知道该选哪个,于是又回到任意值。主题文件需要像代码 API 一样维护。
不一定真要在代码旁边写长注释,但评审时应该能说清楚:它服务哪些界面,哪些场景不该使用。比如 brand-action 用于主要操作,不用于成功提示;brand-card 表示常规内容卡片的层级,不用于浮在全屏之上的弹窗。如果名字只有创建者自己能理解,这个 token 还没准备好进入共享主题。
可以在团队内部维护一张很短的账本:
这张表不是为了增加流程,而是为了阻止同一个名字被赋予互相冲突的职责。代码审查时看到 bg-brand-action 铺满整页,就能基于清楚规则讨论,而不是争论“我觉得这个绿也挺好看”。
改值会让所有消费点一起改变,适合品牌升级,但要做视觉回归。改名会同时改变工具类 API,需要批量迁移模板。删除则可能直接让某些类不再产出样式。不要把三件事揉成一句“整理主题”。
稳妥做法是先增加新名字,迁移组件,再删除旧名字。如果产品发布节奏不允许一次完成,可以暂时保留同值别名:
@theme {
--color-brand-action: #1f6b4f;
--color-brand-primary: #1f6b4f; /* 迁移期旧名 */
}组件完成迁移后再删 brand-primary。这会短期增加一个变量,却能让改动分批验证。别名要写清清理时间,否则“临时兼容”很容易变成永久债务。
主题修改至少要放进几个压力场景看:长中文标题会不会挤坏按钮,英文长单词能否断行,键盘焦点是否清楚,暗色背景下次要文字是否仍可读,小屏和宽屏是否在预期断点切换。颜色还要连同悬停、禁用、错误和选中状态一起看。
圆角与阴影也有语义层级。若所有东西都用最大的圆角和最重阴影,token 虽然统一,界面层级仍然混乱。设计语言的目标是限制选择并说明差异,不是把每个属性压成唯一值。
还要留意“能复用”和“应该复用”的差别。两个区域碰巧用了同一个绿色,不代表它们必须绑定:一个是主要操作,另一个是成功状态,未来很可能各自调整。若只因为当前色值相同就共用 brand-green,第一次改版便会陷入“改按钮却连成功提示一起变”的尴尬。语义 token 允许暂时拥有相同的值,因为它们表达的是独立的变更理由。重复一个数值不一定是坏事,错误地绑定两个职责才是。
一次性的活动页、需要完全服从艺术指导的品牌专题、以画布绘制为主的可视化,未必从完整主题系统中获益。它们可能只有少量通用排版,其他视觉规则都高度定制。这时保留小规模 token,加上局部 CSS,通常比硬把每个艺术细节翻译成工具类更清楚。
反过来,后台系统、组件库、多端产品和多品牌站点非常适合主题层,因为同一个决定会被长期、大量消费。是否使用 Tailwind 从来不是信仰问题;先看复用密度、变更频率和团队协作成本,再决定主题要做到多深。
走到这里,你可以用一条很朴素的标准检查主题设计:稳定、重复、需要一起变化的值进入 @theme;会随运行时状态变化的值用普通变量承接,必要时通过 inline 接入工具类;真正一次性的值留在方括号里;缺失的单一 CSS 能力再交给 @utility。这套边界比“禁止任意值”更能让项目长期保持一致。