本篇是《手写一个内容型技术博客站》系列第 6 篇。
一个内容站要长期维护,颜色系统是"地基":如果颜色散落在几十个文件里写死 hex,改一次主题就是一场寻宝游戏。这篇讲 jiusi-notes 的语义色系统——单源 token + 浅暗双套 + Tailwind v4 的 @theme inline,以及两个必须避开的坑。
一、设计:语义 token 单源
核心思想:代码里不写颜色值,只写语义。要表达"这是错误状态",写 text-error,不写 #f43f5e。
所有 token 定义在 src/app/globals.css 一处,浅色一套、暗色一套:
:root {
/* 浅色系:全部 -500 档 */
--primary: #0ea5e9; /* sky-500 主色(天蓝) */
--secondary: #8b5cf6; /* violet-500 */
--success: #14b8a6; /* teal-500 */
--warning: #f97316; /* orange-500 */
--error: #f43f5e; /* rose-500 */
--body: #334155; /* slate-700 正文 */
--card: #ffffff; /* 白色卡片 */
--background: #f8fafc; /* slate-50 页面背景 */
--muted: #64748b; /* slate-500 次要文字 */
--accent-soft: #e0f2fe;/* sky-100 主色浅底 */
--primary-hover: #0284c7; /* sky-700 */
}
.dark {
/* 暗色系:全部 -400 档 */
--primary: #38bdf8; /* sky-400 */
--body: #cbd5e1; /* slate-300 */
--card: #0f172a; /* slate-900 */
--background: #020617; /* slate-950 */
--muted: #94a3b8;
--accent-soft: rgba(56,189,248,0.12);
}统一规律:浅色全 -500、暗色全 -400(暗色底上 -400 更亮、对比度更好)。这套规律让"加一个新 token"有迹可循。
二、接入 Tailwind v4:@theme inline
Tailwind v4 是 CSS-first 配置(无 tailwind.config.js)。要把 CSS 变量变成工具类 bg-primary / text-body,用 @theme inline:
@theme inline {
--color-primary: var(--primary);
--color-secondary: var(--secondary);
--color-success: var(--success);
--color-error: var(--error);
--color-body: var(--body);
--color-card: var(--card);
--color-background: var(--background);
--color-muted: var(--muted);
--color-accent-soft: var(--accent-soft);
/* ... */
}于是代码里直接:
<div className="bg-card text-body border border-cardborder rounded-xl">
<span className="text-primary">标题</span>
<p className="text-muted">次要信息</p>
</div>@theme inline 会把 CSS 变量内联映射成工具类,.dark 里变量值变了,所有用它的元素自动切换,不需要额外写 dark: 变体——因为 bg-card 编译后是 background-color: var(--color-card),而 --color-card 在 .dark 下被覆盖成暗色值。
三、第三方组件色一致(TanStack Table 例子)
后台评论表用了 TanStack Table v8(headless,无样式),它的配色完全靠 Tailwind 工具类喂。因为没有内置色,反而能 100% 对齐语义 token:
// 表头
<th className="text-muted bg-cardborder/50 p-3 text-left">
// 行 hover
<tr className="hover:bg-accent-soft/60">
// 已删除行特殊色
className="bg-cardborder/10"headless 组件的优势就在这:它不绑架你的视觉,所有颜色从站点 token 来,暗色自动适配,无双主题写两遍的问题。
四、坑1:BARE-VAR —— bg-[--var]/N 在暗色下失效
这是 Tailwind v4 最容易踩的坑。
错误写法(看似合理):
className="hover:bg-[--accent-soft]/60"编译产物是:
color-mix(in oklab, --accent-soft 60%, transparent);
/* ^^^^^^^^^^^^
第一个参数是变量名,不是它的值! */color-mix 第一个参数传了个变量名进去而不是值,结果暗色下 --accent-soft 是 rgba(56,189,248,0.12),混出来完全不透明,或者根本不可见。
正确写法:用已映射进 @theme inline 的语义类加透明度,Tailwind v4 对 --color-* token 的 /N 修饰符会正确生成 color-mix(in oklab, var(--accent-soft) N%, transparent):
className="hover:bg-accent-soft/60" // ✅排查法:grep -rn 'bg-\[--\|text-\[--\|border-\[--' src 找任意值裸变量,凡遇 [--var]/N 一律改成语义类。
这个坑特别隐蔽:浅色下
bg-[--accent-soft]/60看起来"差不多正常",一到暗色就现形(不可见或全透明)。所以叫 BARE-VAR——变量名 vs 变量值,一字之差。
五、坑2:半透明填充的暗色适配
语义色 + 透明度的组合在 Tailwind v4 里用 color-mix 实现,浅暗两套 --primary 自动跟随,无需新增配置:
className="bg-primary/10 border-primary/30"编译为:
background-color: color-mix(in oklab, var(--primary) 10%, transparent);
border-color: color-mix(in oklab, var(--primary) 30%, transparent);深浅色都基于同一个 var(--primary),暗色自动变亮。这是"单源 + 透明度"的正确姿势。
注意别混:如果需要更深的强调色(如按钮实底),加新的语义 token(如 --primary-hover)而不是用固定 Tailwind 色(sky-600)——固定色在暗色下会不协调。
六、分层与约定总结
globals.css(唯一真源)
├─ :root 浅色 token
├─ .dark 暗色 token
├─ @theme inline 映射成工具类
└─ 少量组件级语义类(.markdown-table 等自包含样式)
代码中
└─ 只用 bg-primary/text-muted/border-cardborder 这类语义工具类为什么值得:
- 改主题 = 改 globals.css 一处的 token,全站生效
- 第三组件天然对齐(都用语义类)
- 暗色支持免费(token 双套 + @theme inline)
代价:加新颜色要先想 token 命名(是布局色还是语义色);不能用 Tailwind 色板上随手一个 sky-300 凑数。
给普通人的一句话:颜色是设计系统的"税",收一次(定义 token)比每次写死 hex 交一辈子强。
参考
- Tailwind v4 @theme: https://tailwindcss.com/docs/theme
评论
0评论加载中…