起因
设计师要"用户能自选主色"——蓝色 / 紫色 / 红色 / 绿色等 5 个选项,
切换立刻全 UI 跟着变。
传统做法:每个主题独立 stylesheet,user 选 → 切换 <link> href。
重 + 慢 + 全 UI 闪一下。
CSS 变量(custom properties)让"颜色变量化",运行时改 root 上的变量值
→ 所有引用立刻更新,无闪烁、无重新 load。
基础
:root {
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-text: #1f2937;
--color-bg: #ffffff;
}
button.primary {
background: var(--color-primary);
color: white;
}
button.primary:hover {
background: var(--color-primary-hover);
}
JS 改变量:
document.documentElement.style.setProperty('--color-primary', '#9333ea')
// 所有用了 var(--color-primary) 的元素瞬间变紫
多主题 preset
:root {
--color-primary: #2563eb; /* default 蓝 */
--color-primary-hover: #1d4ed8;
}
[data-theme="purple"] {
--color-primary: #9333ea;
--color-primary-hover: #7e22ce;
}
[data-theme="red"] {
--color-primary: #dc2626;
--color-primary-hover: #b91c1c;
}
[data-theme="green"] {
--color-primary: #16a34a;
--color-primary-hover: #15803d;
}
切换:
document.documentElement.setAttribute('data-theme', 'purple')
零延迟切换。
派生:HSL + alpha 灵活
把 primary 拆 H/S/L 分量存:
:root {
--color-primary-h: 220;
--color-primary-s: 90%;
--color-primary-l: 56%;
}
button {
background: hsl(var(--color-primary-h) var(--color-primary-s) var(--color-primary-l));
border: 1px solid hsl(var(--color-primary-h) var(--color-primary-s) var(--color-primary-l) / 0.3);
}
button:hover {
/* hover 加深:l - 10% */
background: hsl(var(--color-primary-h) var(--color-primary-s) calc(var(--color-primary-l) - 10%));
}
只改 hue 就换主题(不用每个 shade 独立定义):
root.style.setProperty('--color-primary-h', '270') // 紫
root.style.setProperty('--color-primary-h', '0') // 红
root.style.setProperty('--color-primary-h', '140') // 绿
省定义。
用 color-mix(CSS Color 5)
button {
background: var(--color-primary);
}
button:hover {
background: color-mix(in srgb, var(--color-primary) 80%, black);
}
color-mix 让浏览器自动算"加深 20%",不需要 hover-specific 变量。
Chrome 111+ / Safari 16.2+ / Firefox 113+ 支持。
用户自定义主色:color picker
function ThemeCustomizer() {
const [hue, setHue] = useState(220)
useEffect(() => {
document.documentElement.style.setProperty('--color-primary-h', hue.toString())
localStorage.setItem('theme-hue', hue.toString())
}, [hue])
return (
<div>
<label>主色 hue</label>
<input type="range" min="0" max="360"
value={hue} onChange={e => setHue(+e.target.value)} />
<div style={{
background: `hsl(${hue} 90% 56%)`,
width: 100, height: 30
}} />
</div>
)
}
slider 拖动 → 全站颜色实时变。
保存到 localStorage → 下次访问还原。
CSS Color 5 高级
:root {
--color-primary: oklch(60% 0.2 250);
}
.text-on-primary {
/* 自动算对比色 */
color: oklch(from var(--color-primary) calc(l > 50% ? 0 : 100%) 0 0);
}
oklch 色彩空间感知更线性,主题派生更自然。
relative color syntax 让 "从 primary 派生 text" 一行写完。
Browser 支持 2024 大多 evergreen 已经 OK。
暗色模式
:root {
--color-bg: white;
--color-text: #111;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #0d1117;
--color-text: #e6edf3;
}
}
/* 手动覆盖 */
[data-mode="dark"] {
--color-bg: #0d1117;
--color-text: #e6edf3;
}
[data-mode="light"] {
--color-bg: white;
--color-text: #111;
}
<html data-mode="dark"> 强制;不设 attr 跟系统。
防止 FOUC
如果初始化 theme 在 React 之后 → 闪一下默认主题再切。
解决:inline script 在 head 顶部立刻设:
<head>
<script>
(function() {
const saved = localStorage.getItem('theme')
if (saved) document.documentElement.setAttribute('data-theme', saved)
})()
</script>
<link rel="stylesheet" href="/main.css">
</head>
页面渲染前 theme attribute 已经在 → 没有 FOUC。
CSS 框架配合
Tailwind 用 CSS variables 已经标准化(v3.3+):
// tailwind.config.ts
theme: {
extend: {
colors: {
primary: 'hsl(var(--color-primary-h) var(--color-primary-s) var(--color-primary-l) / <alpha-value>)',
},
},
}
<button class="bg-primary text-primary-foreground">Click</button>
<!-- bg-primary 自动用 CSS variable -->
切换 variable → 所有 Tailwind class 立刻变。
性能
CSS variable 修改是浏览器层面 reflow / repaint,非常快。
全站千个 element 用同一 variable 切换 < 1 frame(16ms)。
对比"重新 load stylesheet" 切换:
- 重新解析 CSS(10-100ms)
- 全 DOM repaint
- 偶尔 flash
variable 切换胜出。
与 CSS-in-JS 对比
// emotion / styled-components 风格
const StyledButton = styled.button`
background: ${props => props.theme.primary};
`
<ThemeProvider theme={{ primary: 'blue' }}>
<StyledButton>Click</StyledButton>
</ThemeProvider>
CSS-in-JS theming:
- 优点:JS 完整控制 + 静态 type
- 缺点:bundle 大 + 运行时开销 + SSR 复杂
CSS variable:
- 优点:原生 + 零运行时 + 兼容任何框架
- 缺点:无类型 + JS 改时要 string
我个人 2024 后偏向 CSS variable + Tailwind / shadcn 的方案,
CSS-in-JS 用得少了。
限制 / 注意
1. variable 不能用在 @media 条件本身
:root { --break: 768px; }
/* ❌ 不行 */
@media (min-width: var(--break)) { ... }
@media 查询的值必须是 literal。
2. variable 不能改 @keyframes
@keyframes slide {
from { left: 0; }
to { left: var(--target); } /* 这能用 */
}
keyframes 内部用 variable OK;但改 variable 后正在跑的动画不会重新算。
3. 旧 IE / 老 Safari 不支持
IE 11 全不支持;Safari 9.1+ 支持但有 bug。
2024 这些已经不需考虑。
我们的主题系统
:root {
/* 基础 hue 用户可调 */
--hue: 220;
/* 派生:primary 系列 */
--color-primary-50: hsl(var(--hue) 90% 96%);
--color-primary-100: hsl(var(--hue) 90% 90%);
--color-primary-500: hsl(var(--hue) 90% 56%);
--color-primary-700: hsl(var(--hue) 90% 36%);
/* 派生:text on primary(自动选黑/白) */
--text-on-primary: white;
/* 中性色 */
--color-gray-50: oklch(98% 0 0);
--color-gray-500: oklch(50% 0 0);
--color-gray-900: oklch(20% 0 0);
}
@media (prefers-color-scheme: dark) {
:root {
--color-primary-500: hsl(var(--hue) 80% 65%); /* 暗色下提亮 */
}
}
用户改 --hue → 整套主题色协调地变。
设计师 once 调好 hue 与各 shade 关系,用户只动 hue 不破坏视觉一致。
踩过的坑
-
var(--x)没 fallback → variable 没定义时整个声明无效:
css color: var(--missing); /* 整个 color 不生效 */ color: var(--missing, black); /* fallback 到 black */ -
JS setProperty 大小写敏感:variable 名是
--Color-Primary但
setProperty('--color-primary', ...) 设错变量。 -
shadow DOM 隔离:custom element 内部不继承 :root 变量。
需要在 shadow boundary 显式 forward。 -
calc 不能跨单位:
calc(var(--x) * 2px)错(var 已经带单位)。
存 unitless number + 在 calc 加 unit。 -
打印模式:
@media print没单独考虑主题 variable → 打印出来
是 default 颜色。专门@media print覆盖。
登录后参与评论。