← 返回文章列表
技术

用 CSS 变量做一套可切换的明暗主题

不依赖任何框架,几十行 CSS 变量就能实现优雅、不闪烁的深色模式。

深色模式不该需要一个庞大的主题库。CSS 自定义属性(变量)几乎能独立搞定,本文就是这套方案的拆解。

一切颜色都来自变量

第一步,把所有颜色都抽成变量,集中定义在 :root

:root {
  --bg: #ffffff;
  --text: #0d0f14;
  --accent: #2f6bff;
  --border: #e9ebf0;
}

[data-theme='dark'] {
  --bg: #0b0d12;
  --text: #f2f4f8;
  --accent: #5b86ff;
  --border: #222632;
}

之后所有样式只引用变量,绝不写死颜色

body {
  background: var(--bg);
  color: var(--text);
}

切换主题,就只是在 <html> 上改一个 data-theme 属性,整站颜色瞬间联动。

切换的那几行 JS

const next =
  document.documentElement.dataset.theme === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);

localStorage 记住用户的选择,下次访问还在。

解决「闪一下」的问题

最容易被忽略的坑:页面加载时先显示浅色,再「啪」地跳到深色。原因是设置主题的脚本跑得太晚。

解决办法——把这段脚本内联在 <head> 最前面,让它在首次绘制前就执行:

<script is:inline>
  const saved = localStorage.getItem('theme');
  const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
  document.documentElement.dataset.theme =
    saved || (prefersDark ? 'dark' : 'light');
</script>

关键是「在绘制前」。哪怕只晚一帧,用户都会看到那一下刺眼的闪烁。

顺手做的两件小事

  • color-mix() 从主色派生出半透明的背景色,省得再单独定义一堆变量。
  • prefers-reduced-motion 的用户关掉过渡动画,照顾前庭敏感的人。

几十行 CSS,零依赖,换肤丝滑。有时候平台原生的能力,比我们以为的强得多。