用一个 hue 变量管理整站配色
这个站点的配色只有一个可调参数:--hue。把它从 250 改成 20,整站就从紫调变成暖橘调,不需要改任何一处组件样式。
做法不复杂,但需要先放弃一个旧习惯:不要再用十六进制手写颜色。
为什么是 oklch
十六进制和 rgb 都是「机器友好」的写法,人很难从 #8b7ef8 里看出它偏亮还是偏暗。hsl 好一些,但它的亮度并不符合人眼感受——同样是 50% 亮度,黄色看起来比蓝色亮得多。
oklch 把颜色拆成三个直觉维度:
- L(Lightness):感知亮度,0 是黑,1 是白,符合人眼。
- C(Chroma):彩度,0 是灰,数值越大越艳。
- H(Hue):色相角度,0~360。
这意味着只要固定 L 和 C、只动 H,就能得到一组「亮度一致、只是颜色不同」的配色。这正是主题换色需要的性质。
:root {
--primary: oklch(0.7 0.14 250);
}
只留一个变量
把色相抽出来,其余全部引用它:
:root {
--hue: 250;
--primary: oklch(0.7 0.14 var(--hue));
--page-bg: oklch(0.95 0.01 var(--hue));
--card-bg: #ffffff;
--deep-text: oklch(0.25 0.02 var(--hue));
}
注意 --page-bg 和 --deep-text 的彩度都很低(0.01 和 0.02)。它们不是「紫色」,而是「带一点紫的灰」。这一点很关键:大面积背景如果彩度太高,页面会显得脏,文字读久了也累。
我的经验是——背景彩度别超过 0.015,正文颜色别超过 0.02,彩色只留给强调元素。
暗色模式不是反色
最简单的暗色做法是把亮度反过来,结果通常很糟:白底变纯黑、深色文字变纯白,对比度过高,看一会儿眼睛就疼。
更舒服的做法是保留色相和低彩度,只把亮度整体下移,同时把强调色提亮一点:
:root.dark {
--page-bg: oklch(0.16 0.014 var(--hue));
--card-bg: oklch(0.23 0.015 var(--hue));
--deep-text: oklch(0.94 0.01 var(--hue));
--primary: oklch(0.75 0.14 var(--hue));
}
卡片比页面亮一点点,层级就出来了,不需要靠描边和阴影硬撑。正文不用纯白,用 0.94 左右的亮度,长时间阅读会舒服很多。
阴影在暗色下几乎无效
浅色界面里,卡片靠阴影浮起来;暗色界面里,黑色的阴影落在深色背景上根本看不见。这时候应该改用「更亮的表面」来表达层级——这也是上面把卡片亮度设为 0.23 的原因。
让色相可调
如果想让读者自己选主题色,只需要在根元素上改这一个变量:
const root = document.documentElement;
function setHue(deg) {
root.style.setProperty('--hue', deg);
localStorage.setItem('hue', deg);
}
// 色相滑块
slider.addEventListener('input', (e) => setHue(e.target.value));
因为所有颜色都是 oklch(... var(--hue)) 推导出来的,改一个值,背景、按钮、标签、链接、目录高亮会同时跟着变,而且彼此之间的明暗关系完全保持不变。
几个踩过的小坑
- 别在所有地方都用满彩度。 主色 0.14 已经够艳了,用在按钮和标签上正好,用在大面积色块上会刺眼。
- 纯黑纯白尽量少用。
#000和#fff在 oklch 体系里会觉得「跳出来」,用 0.16 和 0.98 更协调。 - 记得给降级方案。 oklch 在现代浏览器已经可用,但如果需要照顾老环境,可以用
@supports包一层 rgb 兜底。 - 过渡动画要单独处理。 切换主题时给
background-color和color加 200ms 过渡,画面才不会「啪」地跳一下。
小结
这套方法的本质是把「配色」从「一堆写死的值」变成「一个有规则的函数」。写死的东西每增加一个组件就要多调一次,而有规则的东西可以无限扩展。
现在我给新项目起手的第一件事,就是先把这几个 --hue 变量写进 :root。
本文采用 CC BY-NC-SA 4.0 许可协议,最后更新于 2026-10-12。