本文为通用技术教程,示例均可在任意现代浏览器中复现。
目标:换的不只是颜色,是“角色”
主题系统常见的做法是给每个组件写一套 .theme-dark .card { ... } 覆盖。组件一多就会失控。更好的抽象是把颜色拆成角色(role)而不是位置:
--accent-primary:主强调色(链接、主按钮、聚焦环)--accent-primary-hover/--accent-primary-strong:交互态--surface-*/--text-*/--border-*:中性层
组件永远只消费角色 token。换主题时,换的是角色的取值,而不是任何组件样式。
关键技巧:RGB 三元组 + alpha 组装
如果直接存 #38e8c6,透明度变体(hover 蒙层、发光、软背景)就没法复用。把颜色存成 R G B 三个数字,使用时再组装透明度:
:root {
--accent-primary: 56 232 198;
--accent-primary-soft: rgb(var(--accent-primary) / 0.12);
--accent-primary-glow: rgb(var(--accent-primary) / 0.32);
}
.button {
border-color: rgb(var(--accent-primary) / 0.55);
box-shadow: 0 0 48px rgb(var(--accent-primary) / 0.09);
}
这样一套 token 就覆盖了实色、软背景、光晕、边框等所有透明度层级,主题切换只需要覆盖一个三元组。
换肤:属性覆盖 + 级联
在 html 元素上用 data-* 属性承载当前主题,CSS 里按属性选择器覆盖角色 token:
html[data-accent='coral'] {
--accent-primary: 255 122 144;
--accent-primary-hover: 255 158 174;
--accent-primary-strong: 214 74 98;
}
注意两个细节:
- 只覆盖角色,不覆盖派生值。
--accent-primary-soft这类派生 token 引用了--accent-primary,自定义属性是运行时求值的,基础值变了派生值自动跟随——这正是分层 token 的意义。 - 选择器挂在
html上而不是:root之外的元素,因为:root就是html,属性选择器与它同级特异性更高,且随 DOM 属性切换即时生效。
JavaScript 侧只需一行:
document.documentElement.dataset.accent = 'coral';
没有重排、没有组件遍历、没有 CSSOM 注入。Canvas 里也能吃到同一套颜色:
const triplet = getComputedStyle(document.documentElement)
.getPropertyValue('--accent-primary')
.trim();
ctx.fillStyle = `rgb(${triplet} / 0.8)`;
首帧不闪烁
从 localStorage 恢复主题的脚本必须在首帧绘制之前执行,否则用户会看到一次默认色闪现。把内联脚本放在 <head> 顶部、任何样式表生效之前:
<head>
<script is:inline>
(() => {
try {
const { theme } = JSON.parse(
localStorage.getItem('theme-prefs') || '{}',
);
if (['coral', 'cream', 'green'].includes(theme)) {
document.documentElement.dataset.accent = theme;
}
} catch {
/* 保持默认 */
}
})();
</script>
<!-- 样式表在之后加载 -->
</head>
使用 SPA 路由(或 Astro View Transitions)时还有一个隐藏利好:document.documentElement 跨导航持续存在,data-accent 属性不会因为换页丢失,无需在每个页面重复恢复。
无障碍校验
换肤改变了强调色,等于改变了对比度。上线前对每套主题过一遍:
- 主色上的正文/按钮文字(浅色主配深字时确认对比度 ≥ 4.5:1)。
- 彩色文字与其背景(提示:深底上的强调色文字看“亮度”而非“色相”)。
:focus-visible轮廓在所有主题下清晰可辨。
四套主题值一起校验,比上线后再逐个修快得多。
小结
角色化 token + RGB 三元组 + 属性覆盖,三层叠加之后,“全站换肤”从一场组件大改造退化成三行 CSS。Canvas、Web Component、第三方样式都能通过同一套 token 对齐——这就是设计系统里“单一事实来源”的实际形状。