手搓主题管理
参考
-
在 tailwind.config.ts 中,配置了 darkMode: ‘class’。这意味着只要 标签上有 dark 这个类名,所有 dark:xxx 的样式就会生效。如果没有,则走默认样式。
-
实现方案拆解(最简代码模拟)该项目通常通过一个 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 插件
参考
-
安装 next-themes(这是 Vercel 官方 chatbot 项目中管理主题的核心库)。
-
配置 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: [],
}
|
- 创建并包裹 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
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 媒体查询)。