两种配色方案,四种模式:原生 CSS 主题切换

发布: (2026年2月25日 GMT+8 05:42)
5 分钟阅读
原文: Dev.to

I’m sorry, but I can’t retrieve the article from the link you provided. Could you please paste the text you’d like translated here? Once you do, I’ll translate it into Simplified Chinese while preserving the formatting and code blocks.

思路

Frontend is finally moving toward vanilla‑ization, and that is the best thing that has happened to the front‑end since Wes Bos published his first courses. Native browser support for themes works absolutely fine and can be enough for many projects, but I just wanted to have fun. I didn’t want just one colour scheme with light and dark variants. I wanted two schemes, each supporting light and dark – four variants in total – and I wanted to achieve this using only native browser features. And it worked.

要求

  • 轻模式和暗模式,由浏览器原生处理
  • 第二套“春季”(或其他)配色方案,使用完全不同的调色板
  • 一个切换按钮,用于在 defaultspring 之间切换
  • 重新加载时不出现错误主题的闪烁
  • 切换时无需页面重新加载

特异性技巧

:root:root:root

如果你使用 styled‑components,可能已经遇到过特异性战争,导致全局 CSS 变量被 styled‑components 注入的样式覆盖。解决办法非常简单:只需将 :root 重复三次。

**注意:**styled‑components 已过时;我未来的目标是摆脱它,但这不是本文的主要话题。

:root:root:root {
  --color-background: light-dark(
    oklch(0.99 0.0105 320.98),
    oklch(13.709% 0.02553 268.319)
  );
  --color-primary: light-dark(
    oklch(70.61% 0.085 271.69),
    oklch(0.79 0.1233 266.14)
  );
  /* …etc */
}

浏览器会根据用户的系统偏好自动选择正确的值——无需 JavaScript。仍需声明 color-scheme 才能生效:

<!-- 示例:在 HTML 中声明 color-scheme -->
<html color-scheme="light dark">

</html>

**重要:**对于尚不支持 light-dark() 的浏览器,请在下面保留一个 @media (prefers-color-scheme: …) 的回退块。层叠机制会优雅地处理它。

Spring 主题 – 单个 CSS 文件,在 <html> 上使用类

对于第二个主题,我想避免使用动态 CSS 导入(它们是异步的,在 Vite 的生产构建中不可靠,并且会增加复杂性)。于是,两个主题都放在同一个 CSS 文件中。

Spring 主题通过在 <html> 元素上添加 .spring 类来覆盖默认变量。关键点在于选择器的特异性:.spring:root:root:root 能胜过 :root:root:root,因为多了一个类。

/* default theme */
:root:root:root {
  --color-background: light-dark(
    oklch(0.99 0.0105 320.98),
    oklch(0.21 0.037 271.06)
  );
  --color-primary: light-dark(
    oklch(70.61% 0.085 271.69),
    oklch(0.79 0.1233 266.14)
  );
}

/* spring theme – wins when .spring is on <html> */
.spring:root:root:root {
  --color-background: light-dark(
    oklch(0.99 0.012 150),
    oklch(0.18 0.05 145)
  );
  --color-primary: light-dark(
    oklch(56.316% 0.10067 150.907),
    oklch(0.72 0.2 145)
  );
}

在两个主题中,亮色和暗色模式仍会自动生效,light-dark() 也会继续正常工作,无论当前激活的是哪个主题。

切换

classList.toggle() 翻转类并返回新的布尔状态。这就是完整的切换逻辑——没有 React 状态、没有上下文、也没有重新渲染。CSS 能立即响应,因为类的变化会立刻反映在 DOM 中。

// UI component
function handleToggle() {
  const isSpring = document.documentElement.classList.toggle("spring");
  localStorage.setItem("theme", isSpring ? "spring" : "default");
}

在加载时应用正确的主题

同步运行此代码 在首次绘制之前,以避免闪烁、布局偏移或 useEffect 时机问题。

// main.tsx
import "./themes.css";

const savedTheme = localStorage.getItem("theme");
document.documentElement.classList.toggle("spring", savedTheme === "spring");

ReactDOM.createRoot(document.getElementById("root")!).render(
  <App />
);

我尝试过但未成功的方案

在找到这个解决方案之前,我尝试了 @container style() 查询——这是一项较新的 CSS 功能,允许你根据父元素上自定义属性的值来应用样式:

@container style(--theme: spring) {
  body {
    --color-background: …;
  }
}

它在 Chrome 中有效,但在 Firefox Developer Edition 中无效,Safari 完全不支持。因此我放弃了它,但值得在未来关注;它本会是更优雅的做法。

结果

  • 一个 CSS 文件,包含 所有四种主题变体(light default、dark default、light spring、dark spring)
  • 零 JavaScript 用于颜色值——所有颜色都在 CSS 中
  • 没有用于主题的 React 上下文或状态
  • 切换时无需页面重新加载
  • 加载时不会出现错误主题的闪烁
  • 浏览器原生处理浅色/深色偏好
0 浏览
Back to Blog

相关文章

阅读更多 »

Algorhymer的故事:寻找Nessie

又一本带原声带的 Computer Science flipbook!上次我们做了一个带 cold opening 的 time traveling Action Movie https://dev.to/algorhymer/tales-of-the-al