前端主题切换方案

手搓主题管理

参考

  1. 在 tailwind.config.ts 中,配置了 darkMode: ‘class’。这意味着只要 标签上有 dark 这个类名,所有 dark:xxx 的样式就会生效。如果没有,则走默认样式。

  2. 实现方案拆解(最简代码模拟)该项目通常通过一个 ThemeContext 或直接在根组件维护状态。以下是它实现的最简逻辑:

第一步:定义全局样式 (globals.css) 它大量使用 CSS 变量(CSS Variables),这样切换模式时,只需要改变量值,不需要改 HTML 结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
:root {
	--background: #ffffff;
	--foreground: #000000;
}

.dark {
	--background: #09090b;
	--foreground: #ffffff;
}

body {
	background: var(--background);
	color: var(--foreground);
}

第二步:编写切换逻辑 (ThemeSwitcher.tsx) 这是它最核心的 JS 逻辑,直接通过 document.documentElement.classList 操作根节点。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
'use client'

import { useEffect, useState } from 'react'

export function ThemeToggle() {
const [isDark, setIsDark] = useState(false)

// 1. 初始化:检查本地存储或系统偏好
useEffect(() => {
const savedTheme = localStorage.getItem('theme')
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches

    if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
      setIsDark(true)
      document.documentElement.classList.add('dark')
    }

}, [])

// 2. 切换函数
const toggleTheme = () => {
if (isDark) {
document.documentElement.classList.remove('dark')
localStorage.setItem('theme', 'light')
} else {
document.documentElement.classList.add('dark')
localStorage.setItem('theme', 'dark')
}
setIsDark(!isDark)
}

return (
<button onClick={toggleTheme}>
{isDark ? '🌙' : '☀️'}
</button>
)
}

零依赖:不需要额外安装 10KB+ 的库,减少 Bundle Size。

绝对控制权:在 RAG(检索增强生成)场景下,UI 往往需要根据流式传输的内容快速响应,手写逻辑可以避开 React 并发渲染时可能产生的微小延迟。

next-themes 插件

参考

  1. 安装 next-themes(这是 Vercel 官方 chatbot 项目中管理主题的核心库)。

  2. 配置 Tailwind CSS

在 tailwind.config.js 中,必须开启 class 模式,这样 Tailwind 才会根据 标签上的 dark 类名来应用暗色样式。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/** @type {import('tailwindcss').Config} \*/
module.exports = {
darkMode: 'class', // 关键步骤:开启类名切换模式
content: [
"./app/**/_.{js,ts,jsx,tsx}",
"./components/\*\*/_.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}
  1. 创建并包裹 Provider

在 Next.js 的根布局文件 app/layout.tsx 中,使用 ThemeProvider 包裹整个应用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import { ThemeProvider } from 'next-themes'

export default function RootLayout({ children }) {
return (

<html lang="en" suppressHydrationWarning>
{/_ suppressHydrationWarning 是必须的,防止服务端和客户端渲染不匹配导致的闪烁 _/}
<body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
</ThemeProvider>
</body>
</html>
)
}
  1. 编写切换按钮组件

现在可以创建一个按钮来控制主题了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
'use client'

import { useEffect, useState } from 'react'

export function ThemeToggle() {
  const [isDark, setIsDark] = useState(false)

  // 1. 初始化:检查本地存储或系统偏好
  useEffect(() => {
    const savedTheme = localStorage.getItem('theme')
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches

    if (savedTheme === 'dark' || (!savedTheme && prefersDark)) {
      setIsDark(true)
      document.documentElement.classList.add('dark')
    }
  }, [])

  // 2. 切换函数
  const toggleTheme = () => {
    if (isDark) {
      document.documentElement.classList.remove('dark')
      localStorage.setItem('theme', 'light')
    } else {
      document.documentElement.classList.add('dark')
      localStorage.setItem('theme', 'dark')
    }
    setIsDark(!isDark)
  }

  return (
    <button onClick={toggleTheme}>
      {isDark ? '🌙' : '☀️'}
    </button>
  )
}

核心原理解析(硬核干货):

LocalStorage 持久化:next-themes 会自动将用户选择的主题存入浏览器的 localStorage。

防止“闪烁”(Flash of unstyled content):这是最难点。next-themes 会在 渲染之前注入一小段阻止阻塞的脚本,通过读取本地存储瞬间给 加上 dark 类名,确保用户不会在加载时看到白屏闪烁。

系统级跟随:通过 enableSystem 属性,应用可以自动识别并跟随操作系统的暗色模式设置(通过 prefers-color-scheme 媒体查询)。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计