全局配置项目主题色(主题切换)
在 React + TypeScript 项目中实现 全局主题切换(暗黑/亮色模式) 通常有三种常见方案:
1️⃣ CSS变量方案(推荐,简单易维护) 适合需要动态主题切换和性能要求高的项目;
2️⃣ CSS-in-JS(如 styled-components、Emotion)方案
3️⃣ UI库自带方案(如 Ant Design、MUI 的 ThemeProvider)
一、总体思路
全局主题切换的核心是:
不重新渲染页面,通过切换
data-theme
或class
来控制 CSS 变量,从而动态改变颜色。
目录结构
css
src/
├─ context/
│ └─ ThemeContext.tsx
├─ styles/
│ └─ globals.css
├─ index.css
├─ App.tsx
├─ index.tsx
themes/
├─ dark.css
├─ light.css
二、实现步骤
1️⃣ 定义全局主题变量
创建 themes/dark.css
:
css
html[data-theme="dark"] {
--primary: #5dade2;
--secondary: #58d68d;
--accent: #e74c3c;
--background: #121212;
--surface: #1e1e1e;
--text: #e0e0e0;
--text-secondary:
创建 themes/light.css
:
css
html[data-theme="light"] {
--primary: #2980b9;
--secondary: #1abc9c;
--accent: #f39c12;
--background: #ecf0f1;
--surface: #ffffff;
--text: #2c3e50;
--text-secondary:
创建 src/styles/globals.css
:
css
@import '../themes/dark.css';
@import '../themes/light.css';
body {
font-family: sans-serif;
background-color: var(--background);
padding: 20px;
color: var(--text);
}
button {
background-color: var(--primary);
color: var(--surface);
}
:root {
--primary: #3498db;
--secondary: #2ecc71;
--background: #f8f9fa;
--surface: #ffffff;
--text: #333333;
--border: #e0e0e0;
}
2️⃣ 在全局样式中使用变量
在 src/index.css
中:
css
body {
font-family: sans-serif;
background-color: var(--background);
padding: 20px;
color: var(--text);
}
button {
background-color: var(--primary);
color: var(--surface);
}
3️⃣ 在 React 中控制主题
实现思路
- 定义一个
ThemeContext
,保存当前主题(light/dark)和修改函数。 - 用
ThemeProvider
组件包裹整个应用。 - 在任何组件中通过
useContext
获取当前主题和切换方法。 - 通过设置
<html data-theme="">
属性或 CSS 变量来动态切换主题。
在src/context/ThemeContext.tsx
中, 创建 ThemeContext
tsx
import React, { createContext, useContext, useState, useEffect } from "react";
type Theme = "light" | "dark";
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [theme, setThemeState] = useState<Theme>(
(localStorage.getItem("theme") as Theme) || "light"
);
// 切换主题
const toggleTheme = () => {
const newTheme = theme === "light" ? "dark" : "light";
setThemeState(newTheme);
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
};
// 设置主题
const setTheme = (newTheme: Theme) => {
setThemeState(newTheme);
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
};
// 初始加载时同步
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
// 封装一个 Hook 方便使用
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
4️⃣在入口文件包裹 Provider
在src/index.tsx
中
tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import { ThemeProvider } from "./context/ThemeContext";
import "./styles/globals.css";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<ThemeProvider>
<App />
</ThemeProvider>
);
5️⃣在组件中使用主题切换
tsx
import React from "react";
import { useTheme } from "./context/ThemeContext";
const App: React.FC = () => {
const { theme, toggleTheme } = useTheme();
return (
<div style={{ padding: 20 }}>
<h1>🌗 当前主题:{theme}</h1>
<button onClick={toggleTheme}>
切换为 {theme === "light" ? "暗黑模式" : "亮色模式"}
</button>
<p>这是示例文本,会随主题颜色变化。</p>
</div>
);
};
export default App;
说明:
-
初次加载时会根据
localStorage
读取上次主题。 -
点击按钮后会:
- 修改 React state
- 更新
<html data-theme="dark">
- 自动切换 CSS 变量样式
整个应用的主题瞬间切换,无需刷新 。
三、支持系统偏好(跟随系统主题)
系统主题检测说明
js
const media = window.matchMedia("(prefers-color-scheme: dark)");
window.matchMedia()
是浏览器API,用于检测媒体查询(prefers-color-scheme: dark)
查询系统是否启用深色模式media.matches
返回布尔值:true
表示系统是深色模式,false
表示浅色模式
src/context/ThemeContext.tsx
tsx
import React, {
createContext,
useContext,
useEffect,
useState,
useCallback,
} from "react";
type Theme = "light" | "dark";
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
setTheme: (theme: Theme) => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const getInitialTheme = (): Theme => {
const local = localStorage.getItem("theme") as Theme | null;
if (local) return local;
// 没有保存的主题时,根据系统偏好
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
return prefersDark ? "dark" : "light";
};
const [theme, setThemeState] = useState<Theme>(getInitialTheme);
/** 设置主题(含保存与更新 DOM) */
const setTheme = useCallback((newTheme: Theme) => {
setThemeState(newTheme);
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
}, []);
/** 切换主题 */
const toggleTheme = useCallback(() => {
setTheme(theme === "light" ? "dark" : "light");
}, [theme, setTheme]);
/** 初始加载时同步 */
useEffect(() => {
document.documentElement.setAttribute("data-theme", theme);
}, [theme]);//必须监听 theme,目的是同步 DOM 与 React 状态
/** 监听系统主题变化 */
useEffect(() => {
const media = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = (e: MediaQueryListEvent) => {
const systemTheme: Theme = e.matches ? "dark" : "light";
const savedTheme = localStorage.getItem("theme");
// 仅当用户没有手动设置主题时,才跟随系统
if (!savedTheme) {
setTheme(systemTheme);
}
};
media.addEventListener("change", handleChange);
return () => media.removeEventListener("change", handleChange);
}, [setTheme]);//如果未来 `setTheme` 改变引用(例如 ThemeProvider 重新创建),effect 会重新绑定监听,保持逻辑一致。
return (
<ThemeContext.Provider value={{ theme, toggleTheme, setTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context) throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
四、三种模式(跟随系统 / 亮色 / 暗色)的版本
tsx
import React, {
createContext,
useContext,
useState,
useEffect,
useCallback,
} from "react";
type ThemeMode = "light" | "dark" | "system";
interface ThemeContextType {
mode: ThemeMode; // 当前模式
theme: "light" | "dark"; // 实际应用的主题
setMode: (mode: ThemeMode) => void;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const getInitialMode = (): ThemeMode => {
const saved = localStorage.getItem("themeMode") as ThemeMode | null;
return saved || "system";
};
const [mode, setModeState] = useState<ThemeMode>(getInitialMode);
const [theme, setTheme] = useState<"light" | "dark">("light");
/** 根据 mode 和系统设置决定最终主题 */
const applyTheme = useCallback(
(currentMode: ThemeMode) => {
if (currentMode === "light") {
setTheme("light");
document.documentElement.setAttribute("data-theme", "light");
} else if (currentMode === "dark") {
setTheme("dark");
document.documentElement.setAttribute("data-theme", "dark");
} else {
const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const systemTheme = prefersDark ? "dark" : "light";
setTheme(systemTheme);
document.documentElement.setAttribute("data-theme", systemTheme);
}
},
[]
);
/** 设置模式(用户选择) */
const setMode = useCallback(
(newMode: ThemeMode) => {
setModeState(newMode);
localStorage.setItem("themeMode", newMode);
applyTheme(newMode);
},
[applyTheme]
);
/** 手动切换亮/暗模式(不影响系统模式) */
const toggleTheme = useCallback(() => {
if (mode === "system") {
// 用户在系统模式下切换 → 固定到相反的主题(如果不做mode值区分,setMode("system" === "light" ? "dark" : "light");第一次切换一定是light)
setMode(theme === "light" ? "dark" : "light");
} else {
setMode(mode === "light" ? "dark" : "light");
}
}, [mode, theme, setMode]);
/** 初始化:同步主题 */
useEffect(() => {
applyTheme(mode);
}, [mode, applyTheme]);
/** 系统主题变化时,若为 system 模式则自动跟随 */
useEffect(() => {
const media = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = (e: MediaQueryListEvent) => {
if (mode === "system") {
const systemTheme = e.matches ? "dark" : "light";
setTheme(systemTheme);
document.documentElement.setAttribute("data-theme", systemTheme);
}
};
media.addEventListener("change", handleChange);
return () => media.removeEventListener("change", handleChange);
}, [mode]);
return (
<ThemeContext.Provider value={{ mode, theme, setMode, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (!context)
throw new Error("useTheme must be used within a ThemeProvider");
return context;
};
在App.tsx
中
tsx
import React from "react";
import { useTheme } from "./context/ThemeContext";
const App: React.FC = () => {
const { mode, theme, setMode, toggleTheme } = useTheme();
return (
<div style={{ padding: "2rem" }}>
<h1>🌗 React 三种主题模式</h1>
<p>当前模式:{mode}</p>
<p>当前实际主题:{theme}</p>
<div style={{ display: "flex", gap: "10px", marginBottom: "20px" }}>
<button onClick={() => setMode("light")}>亮色模式</button>
<button onClick={() => setMode("dark")}>暗色模式</button>
<button onClick={() => setMode("system")}>跟随系统</button>
<button onClick={toggleTheme}>手动切换主题</button>
</div>
<p>示例文字会根据主题自动变色。</p>
</div>
);
};
export default App;
五、其他说明
1. html[data-theme="dark"]
与[data-theme="dark"]
区别

2.@import
和 import
区别
3.设置主题和切换主题为什么要用 useCallback?可以不用吗?
useCallback 是为了避免不必要的重新渲染。因为这两个函数会被传递给 Context,如果不使用 useCallback,每次组件重新渲染时都会创建新的函数,导致消费 Context 的子组件不必要的重新渲染。使用 useCallback 可以缓存函数,只有在依赖项变化时才更新函数。
使用 useCallback 的原因:
useCallback
可以 缓存函数引用 ,避免每次渲染时都创建新函数 (创建新函数会导致使用了useTheme()
子组件重新渲染),防止因引用变化导致子组件不必要地重新渲染。
- 性能优化:避免不必要的子组件重渲染
- 依赖管理:在 useEffect 中作为依赖时保持引用稳定
- 最佳实践:对于传递给 context 的函数,保持稳定引用
👉 为什么有用?
- 如果你的
ThemeContext
被很多组件订阅,
那么没有useCallback
时,每次ThemeProvider
重新渲染,
toggleTheme
/setTheme
都会是新的函数引用,
导致所有useTheme()
的组件都重渲染。
在性能敏感或 Context 使用广泛的项目中,
useCallback
是非常推荐的。
⚙️ 如果不写 useCallback 会怎样?
实际上也不会出错,主题切换功能仍然能正常工作。
只是:
- 每次
ThemeProvider
渲染都会创建新函数引用; - 所有用到
useTheme()
的组件都会检测到 context 值变化; - 这可能导致 性能浪费(小项目影响不大,大项目明显) 。
4.切换主题需要监听 setTheme
吗?
- 这里的
setTheme
是一个useCallback
生成的函数,并不是useState
中改变状态的setState
;同时useCallback
监听依赖性变化是重新创建函数,而不是像useEffect
一样去执行一遍! useCallback
的依赖数组是[theme, setTheme]
。这意味着当theme
或setTheme
发生变化时,toggleTheme
函数会被重新创建。- 为什么需要依赖
theme
?
因为函数内部使用了theme
的值,如果不在依赖数组中包含theme
,那么当theme
变化时,toggleTheme
函数闭包中的theme
还是旧值,导致切换逻辑错误。 - 为什么需要依赖
setTheme
?
因为setTheme
是稳定的(比如用 useCallback 包装且依赖为空),但为了确保总是调用最新的setTheme
,将其作为依赖是安全的。