sY.zhang.
博客

CSS Custom Properties 在主题切换中的几个坑

暗色模式不只是换个颜色,CSS 变量切换时还要处理 transition、initial value、SSR 闪烁等问题。

1 分钟阅读

最近给博客加暗色模式,遇到几个不直观的坑,记下来。

1. 闪烁问题(FOUC)

最常见的坑:页面加载时,先用默认主题渲染了一帧,然后 JS 才切换到用户选择的主题,导致一闪而过。

解决方法:在 <head> 里加一段同步执行的 inline script,在第一次 paint 之前就把 dark 类名加上:

<script>
  (function () {
    const stored = localStorage.getItem('theme');
    const prefersDark = matchMedia('(prefers-color-scheme: dark)').matches;
    if (stored === 'dark' || (!stored && prefersDark)) {
      document.documentElement.classList.add('dark');
    }
  })();
</script>

关键是 is:inline,否则 Astro 会把它当模块处理,等到 DOM 加载完才执行。

2. Transition 的副作用

我加了一个 transition: background-color 0.3sbody 上,切主题时整页有个柔和过渡。

但当用户滚动到一半切主题时,整页重绘过渡会卡顿。原因是 transition 触发了合成层重新分配。

解决:用 view-transition-api 替代 CSS transition,或者只在初始状态用 transition,切换瞬间临时禁用:

html.theme-changing * {
  transition: none !important;
}

3. Tailwind v4 的 dark variant

Tailwind v4 默认 dark variant 是 prefers-color-scheme。要改成基于类名,得用 @custom-variant

@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));

写完这个,dark: 修饰符才认 .dark 类。

4. 颜色 token 的层级

我习惯这样组织:

@theme {
  --color-paper: #faf7f2;
  --color-night: #0f0f0e;
}

:root {
  --bg: var(--color-paper);
}
.dark {
  --bg: var(--color-night);
}

Tailwind 的 @theme 是设计 token 层(不随主题变),:root / .dark 是语义层(随主题变)。组件只用语义变量,不直接消费设计 token。

5. 系统主题变化时跟随

如果用户没显式选过主题,应该跟随系统:

matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
  if (!localStorage.getItem('theme')) {
    document.documentElement.classList.toggle('dark', e.matches);
  }
});

加这一行,桌面切换深色模式时网站会自动跟着变,体验加分。

小结

暗色模式看着简单,做扎实要考虑:闪烁、过渡、SSR、Tailwind 适配、系统跟随。 不是难,是细节多。