Skip to content

Next.js + Tailwind 主题切换原理详解 ​

一、核心概念 ​

主题切换的本质:通过修改 <html> 标签的 class,让 CSS 选择器自动匹配不同的样式


二、完整流程演示 ​

步骤1:你在组件中写的代码 ​

tsx
<div className="bg-white dark:bg-gray-800 text-black dark:text-white">
  这是一段文字
</div>

步骤2:Tailwind 编译后的 CSS ​

css
/* 日间模式样式(默认) */
.bg-white {
  background-color: white;
}

.text-black {
  color: black;
}

/* 夜间模式样式(需要父级有 .dark class) */
.dark .dark\:bg-gray-800 {
  background-color: #1f2937;
}

.dark .dark\:text-white {
  color: white;
}

关键点: .dark .dark\:bg-gray-800 中间有空格,这是 CSS 的后代选择器

步骤3:实际的 HTML 结构 ​

日间模式时: ​

html
<html class="light">
  <body>
    <div class="bg-white dark:bg-gray-800 text-black dark:text-white">
      这是一段文字
    </div>
  </body>
</html>

CSS 匹配结果:

  • ✅ .bg-white 匹配 → 白色背景
  • ✅ .text-black 匹配 → 黑色文字
  • ❌ .dark .dark:bg-gray-800 不匹配(因为 html 是 light,不是 dark)
  • ❌ .dark .dark:text-white 不匹配

最终效果:白底黑字

夜间模式时: ​

html
<html class="dark">
  <body>
    <div class="bg-white dark:bg-gray-800 text-black dark:text-white">
      这是一段文字
    </div>
  </body>
</html>

CSS 匹配结果:

  • ✅ .bg-white 匹配 → 白色背景
  • ✅ .text-black 匹配 → 黑色文字
  • ✅ .dark .dark:bg-gray-800 匹配!→ 灰色背景(覆盖白色)
  • ✅ .dark .dark:text-white 匹配!→ 白色文字(覆盖黑色)

最终效果:灰底白字


三、CSS 后代选择器详解 ​

什么是后代选择器? ​

css
.父级 .子级 {
  /* 样式 */
}

意思是:在 class 为 父级 的元素内部,找到 class 为 子级 的所有后代元素

实际例子 ​

html
<div class="container">
  <p class="text">我会变红</p>
  <div>
    <p class="text">我也会变红(孙子元素)</p>
  </div>
</div>

<p class="text">我不会变红(不在 container 内)</p>
css
.container .text {
  color: red;
}

应用到主题切换 ​

css
.dark .dark\:bg-gray-800 {
  background-color: #1f2937;
}

翻译成人话:

  • 在 class 为 dark 的元素内部
  • 找到 class 为 dark:bg-gray-800 的所有后代元素
  • 给它们应用灰色背景

四、next-themes 做了什么? ​

1. 修改 <html> 的 class ​

tsx
// 用户点击切换按钮
setTheme("dark")

// next-themes 执行:
document.documentElement.className = "dark"

结果:

html
<html class="dark">

2. 保存到 localStorage ​

js
localStorage.setItem('theme', 'dark')

下次刷新页面时自动恢复主题

3. 防止闪烁 ​

在 <head> 注入脚本,页面加载前就设置 class:

html
<script>
  // 页面加载前立即执行
  const theme = localStorage.getItem('theme')
  if (theme === 'dark') {
    document.documentElement.classList.add('dark')
  }
</script>

五、为什么所有组件都能自动切换? ​

不是 next-themes 给每个组件加效果,而是浏览器的 CSS 引擎自动匹配!

类比理解 ​

就像你写:

css
button:hover {
  background: blue;
}

浏览器会自动检测鼠标悬停,不需要你手动给每个 button 加效果。

同理,dark: 前缀也是浏览器自动检测 <html class="dark">,不需要手动操作每个组件。


六、完整切换流程图 ​

用户点击切换按钮
    ↓
setTheme("dark")
    ↓
next-themes 修改 <html class="dark">
    ↓
浏览器重新计算 CSS
    ↓
所有 .dark .dark:xxx 选择器生效
    ↓
页面上所有带 dark: 前缀的样式自动应用
    ↓
主题切换完成!

七、实际代码示例 ​

Tailwind 配置(已配置好) ​

js
// tailwind.config.js
module.exports = {
  darkMode: 'class', // 使用 class 模式
}

组件中使用 ​

tsx
// 任何组件都可以这样写
<div className="bg-white dark:bg-gray-900">
  <h1 className="text-black dark:text-white">标题</h1>
  <p className="text-gray-600 dark:text-gray-300">内容</p>
</div>

切换按钮 ​

tsx
import { useTheme } from "next-themes"

function ThemeToggle() {
  const { setTheme, resolvedTheme } = useTheme()
  
  return (
    <button onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
      切换主题
    </button>
  )
}

八、常见问题 ​

Q1: 为什么要在 <html> 上加 class,不能在 <body> 上吗? ​

A: 可以,但 <html> 是最顶层元素,确保所有内容都能被选择器覆盖。

Q2: 如果我不用 Tailwind,能用 next-themes 吗? ​

A: 可以!你可以手动写 CSS:

css
/* 日间模式 */
.my-card {
  background: white;
  color: black;
}

/* 夜间模式 */
.dark .my-card {
  background: #1f2937;
  color: white;
}

Q3: 性能会不会有问题? ​

A: 不会。CSS 选择器匹配是浏览器原生能力,非常快。只是改一个 class,浏览器自动重新渲染。


九、总结 ​

  1. next-themes 只做一件事:修改 <html class="dark">
  2. Tailwind 的 dark: 前缀编译成 .dark .dark:xxx 后代选择器
  3. 浏览器 自动匹配 CSS 选择器,应用对应样式
  4. 不需要遍历组件,不需要手动加效果,全靠 CSS 原生能力

这就是为什么一个简单的 class 切换,能让整个网站的主题瞬间改变!