深色与浅色主题最怕的不是做不出来,而是做出来之后两套样式各自生长:改一处忘一处,半年后没人敢动。
语义优先,颜色靠边
组件里不允许出现任何具体颜色——只允许语义:
- 画布
canvas、卡面surface/surface-strong/surface-muted - 文字
ink/ink-2/ink-3/ink-4(按重要度递减) - 发丝线
hairline/hairline-soft - 强调
accent/accent-2及其派生
颜色本身(比如「靛蓝 #6366f1」)只活在主题定义里,组件永远只见语义名。
一套定义,两套取值
:root {
--app-ink: #0f172a; /* 浅色:墨蓝黑 */
}
.dark {
--app-ink: #f4f6fb; /* 深色母版:near-white */
}
再用 Tailwind v4 的 @theme inline 把变量映射成工具类:
@theme inline {
--color-ink: var(--app-ink);
}
inline 的关键在于工具类输出的是 var(--app-ink) 引用而非构建期常量——于是切换 .dark 这个 class,全站即时生效,组件零改动。
组件层的三条纪律
- 禁止
dark:变体。出现dark:就说明某个颜色没被 token 化。 - 禁止内联 hex。同理,颜色只允许以 token 形式出现。
- 对比度按最坏情况算。深浅两套都要过 WCAG AA,尤其注意「最浅文字色 × 最亮背景色」的组合。
派生色怎么办
半透明的「卡面白」「发丝白」这类派生色,直接在 token 层定义透明度版本(rgb(255 255 255 / 0.4)),而不是在使用处临时叠 /40 修饰符——这样深浅两套主题可以给同一个语义名不同的透明度(深色下发丝线 12%,浅色下 40%,视觉密度才对等)。
小结
主题系统的本质是把「颜色决策」从组件层挪走。组件只回答「这里是主文字还是次要文字」,主题层回答「主文字在这套主题下是什么颜色」。决策分层了,两套主题才可能永远同步。