站点做到一定程度,一定会遇到这两个需求:换配色、加暗色模式。如果颜色散落在几十个组件里,这两件事都会变成体力活,而且极容易改漏。
两层变量结构
关键是把变量分成两层,各司其职:
:root {
/* 第一层:色板。只定义"有什么颜色",不定义"用在哪里" */
--p-navy-700: #14487e;
--p-red-600: #c22b23;
--p-gray-200: #dfe4ea;
/* 第二层:语义。组件只认这一层 */
--bg-surface: #ffffff;
--fg-heading: #1c2024;
--border: var(--p-gray-200);
--link: var(--p-navy-600);
}
好处是:加暗色模式时,只需要重新赋值语义层,页面结构完全不用动:
html[data-theme='dark'] {
--bg-surface: #171d25;
--fg-heading: #eef2f6;
--border: #2b333e;
--link: #7cb6ef;
}
三种无障碍模式的实现
政务类站点通常需要字号调节、高对比度、纯文本模式。用同一套思路,把它们都做成“给 <html> 加属性”,样式层面自动响应:
| 模式 | 属性 | 实现方式 |
|---|---|---|
| 字号调节 | data-fs="s|m|l" |
改写 --fs-scale,所有字号基于它计算 |
| 高对比度 | data-contrast="high" |
覆写为黑底白字 + 黄色链接 |
| 纯文本 | data-plain="on" |
隐藏装饰元素、取消阴影与圆角 |
字号的做法值得展开一句:把字号定义成 calc(14px * var(--fs-scale)),那么调节字号只需要改一个系数,全站文字同步缩放。
:root {
--fs-scale: 1;
--fs-base: calc(15px * var(--fs-scale));
}
html[data-fs='l'] {
--fs-scale: 1.15;
}
两个容易踩的坑
1. 首屏闪烁
主题存在 localStorage 里、由 JS 读取,就会出现“先渲染浅色、再跳成深色”的闪白。解决办法是在 <head> 里放一段内联脚本,在页面渲染前就把属性写上去:
<script is:inline>
(function () {
var el = document.documentElement;
var saved = JSON.parse(localStorage.getItem('prefs') || '{}');
var dark = saved.theme === 'dark' ||
(saved.theme !== 'light' && matchMedia('(prefers-color-scheme: dark)').matches);
el.dataset.theme = dark ? 'dark' : 'light';
})();
</script>
内联脚本要尽量短,因为它是阻塞渲染的;同时注意它与主逻辑里的存储键必须保持一致。
2. 语义变量不够用
一开始容易只定义 --bg、--fg 两三个变量,做到一半发现导航条、页脚、引用块在暗色下都不对劲,只好回去补。经验是:先把页面上所有“角色”列一遍(页面底、卡片、次级底、描边、正文、次要文字、链接、导航底、导航文字、页脚底……),再开始定义。
小结
设计令牌的本质不是“变量多”,而是把决策点集中起来。集中之后,改配色是改文件,而不是改几十个组件。