主题切换最容易做成「每个组件写两套颜色」。更稳妥的做法是:组件只消费语义化变量,真正换肤只改一处 token。

先定义语义 token

:root 里放「含义」而不是「色号名字」:

:root {
  --bg: #f5f7f6;
  --text: #1a2e28;
  --accent: #1f7a65;
  --border: #c9ddd4;
}

[data-theme="dark"] {
  --bg: #121a18;
  --text: #e8f0ed;
  --accent: #5cbc9f;
  --border: #2a3d36;
}

组件样式写成:

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

.button-primary {
  background: var(--accent);
}

这样暗色模式几乎不用再改组件文件。

用一行 JS 切换

const root = document.documentElement;
const next = root.getAttribute('data-theme') === 'dark' ? 'light' : 'dark';
root.setAttribute('data-theme', next);
localStorage.setItem('theme', next);

页面加载时读回偏好,避免闪一下:

const saved = localStorage.getItem('theme');
if (saved) document.documentElement.setAttribute('data-theme', saved);
若站点完全静态、不想依赖本地存储,也可以只切 data-theme,刷新后回到默认即可。

几个容易踩的坑

  • 不要在组件里硬编码十六进制,否则换肤永远漏网。
  • 阴影、描边也要进 token,暗色下纯黑阴影往往发脏。
  • 图片与插画可能需要单独处理;纯色 UI 用变量就够了。
  • 可用 color-scheme 提示浏览器原生控件(滚动条、表单)跟随主题。

进阶:系统偏好

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #121a18;
    --text: #e8f0ed;
  }
}

用户手动选择优先,没有选择时跟随系统,体验更自然。

小结

CSS 变量做主题的核心是「一层语义 token + 根节点切换」。依赖少、改动集中,特别适合个人站和中小型项目。等业务真的复杂到多品牌、多密度时,再考虑设计系统也不迟。