React 主题切换与暗色模式:用 CSS 变量 + Context 把主题做成一等公民

test12026-10-021 次阅读

为什么主题不该写死在组件里

很多项目把颜色直接写进组件样式,做到一半要加暗色模式,只能全局搜索替换,还容易漏。更稳的做法是把所有颜色收敛到 CSS 变量,组件只消费变量;主题切换只是切换 :root 上的一组变量。React 负责把当前主题存进 Context,组件用 useTheme() 取用,完全不关心具体色值。

第一步:定义两套变量

在全局样式里放两份变量,暗色主题挂在 [data-theme="dark"] 上。组件里一律用 var(--color-bg),不直接写颜色。

:root {
  --color-bg: #ffffff;
  --color-fg: #1f2328;
  --color-accent: #2563eb;
  --color-border: #e5e7eb;
}

[data-theme="dark"] {
  --color-bg: #0d1117;
  --color-fg: #e6edf3;
  --color-accent: #58a6ff;
  --color-border: #30363d;
}

第二步:用 Context 管理主题

Context 持有当前主题与切换函数,初始值优先读 localStorage,其次跟随系统 prefers-color-scheme。切换时同步写回 data-theme 与存储,保证刷新不丢。

import { createContext, useContext, useEffect, useState } from "react";

type Theme = "light" | "dark";
const ThemeContext = createContext<{ theme: Theme; toggle: () => void } | null>(null);

function getInitialTheme(): Theme {
  const saved = localStorage.getItem("theme");
  if (saved === "light" || saved === "dark") return saved;
  return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const [theme, setTheme] = useState<Theme>(getInitialTheme);

  useEffect(() => {
    document.documentElement.setAttribute("data-theme", theme);
    localStorage.setItem("theme", theme);
  }, [theme]);

  const toggle = () => setTheme((t) => (t === "dark" ? "light" : "dark"));
  return <ThemeContext.Provider value={{ theme, toggle }}>{children}</ThemeContext.Provider>;
}

export function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error("useTheme 必须在 ThemeProvider 内使用");
  return ctx;
}

第三步:组件只消费变量

组件样式通过 CSS 变量取色,切换主题时浏览器自动重算,无需 React 重渲染任何节点。

import { useTheme } from "./theme";

export function Toolbar() {
  const { theme, toggle } = useTheme();
  return (
    <header style={{ background: "var(--color-bg)", color: "var(--color-fg)", borderBottom: "1px solid var(--color-border)" }}>
      <span>当前:{theme}</span>
      <button onClick={toggle} style={{ color: "var(--color-accent)" }}>切换主题</button>
    </header>
  );
}

工程要点

  • 变量只定义在 :root 与 [data-theme="dark"] 两层,主题越多就加一层选择器,组件零改动。
  • 用 useEffect 把主题写到 document.documentElement,避免 SSR 首屏闪烁,可在服务端按 cookie 提前注入 data-theme。
  • 需要"跟随系统"的选项时,把主题状态扩展为 "light" | "dark" | "system",system 下用 matchMedia 监听变化。
  • 不要给主题色加 transition 到全局,只对需要平滑过渡的元素加,否则切换时整页闪一下。
T

test1

文章作者

为什么主题不该写死在组件里 很多项目把颜色直接写进组件样式,做到一半要加暗色模式,只能全局搜索替换,还容易漏。更稳的做...

分类
技术
发布时间
2026-10-02
字数
约 2415 字
阅读
1 次