使用 React Context API、React hooks 和 TailwindCSS 的 Dark Mode 快速实现网页浅色模式和深色模式之间的切换。
此教程需要对 React 的 Context Api 有一定的了解,如果您还不了解,可以前往 使用 Context 深层传递参数 学习。
通过这篇文章,您将学到:
- 使用 React Context 和 localStorage 共享全局状态。
- 暗黑模式切换:浅色、深色、系统。
起步
使用 Vite 创建 React 项目:
shell
PS J:\react-project> pnpm create vite@latest
√ Project name: ... react-dark-mode
√ Select a framework: >> React
√ Select a variant: >> TypeScript + SWC
Scaffolding project in J:\react-project\react-dark-mode...
Done. Now run:
cd react-dark-mode
pnpm install
pnpm run dev
安装依赖、运行项目:
shell
cd react-dark-mode
pnpm install
pnpm run dev
安装 Tailwind CSS、生成 tailwind.config.js
和 postcss.config.js
文件:
csharp
pnpm install -D tailwindcss postcss autoprefixer
pnpm tailwindcss init -p
修改 tailwind.config.js
文件:
js
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: {
extend: {},
},
plugins: [],
};
darkMode 设置为 class,修改 content 添加模板文件路径。
修改 /src/index.css
文件,定义一些基本的样式:
css
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer {
:root {
--background: #ffffff;
--foreground: #09090b;
--border: #e3e3e7;
}
.dark {
--background: #09090b;
--foreground: #f9f9f9;
--border: #27272a;
}
}
@layer base {
* {
@apply box-border border-[--border];
}
body {
@apply bg-[--background] text-[--foreground];
}
}
创建 React Context
新建文件 /src/components/ThemeProvider.tsx
:
tsx
import { createContext, useContext, useEffect, useState } from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme必须在ThemeProvider中使用")
return context
}
- 首先从 localStorege 中获取主题,如果没有找到,则使用默认主题。
- 监听主题状态的变化。每当主题改变时,更新 html 的 class。如果主题为 system,则根据 prefers-color-scheme 来检测系统的主题色设置,给 html 添加对应的 class。
setTheme
函数将新的主题存储在localStorage中,并更新主题状态。- 通过
ThemeProviderContext.Provider
组件将主题状态和setTheme
函数传递给其子组件。
useTheme
是一个自定义Hook,它可以在任何函数组件中访问主题上下文。如果它在ThemeProvider
之外被调用,将抛出一个错误。
修改 main.tsx
:
tsx
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import { ThemeProvider } from "./components/ThemeProvider.tsx";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<ThemeProvider defaultTheme="dark" storageKey="theme">
<App />
</ThemeProvider>
</React.StrictMode>,
);
切换主题
安装 lucide-react 图标库:
shell
pnpm install lucide-react
新建 /src/components/ThemeToggle.tsx
文件:
tsx
import { Sun, Moon } from "lucide-react";
import { useTheme } from "./components/ThemeProvider";
const ThemeToggle = () => {
const { theme, setTheme } = useTheme();
return (
<button
onClick={() => setTheme(theme === "light" ? "dark" : "light")}
className="inline-flex rounded-md p-2 hover:bg-zinc-100 dark:hover:bg-zinc-900"
>
<Sun
size={20}
className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
/>
<Moon
size={20}
className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
/>
</button>
);
};
export default ThemeToggle;
引入 /src/App.tsx
文件:
tsx
import ThemeToggle from "./components/ThemeToggle";
function App() {
return (
<div className="sapce-y-4 mx-auto max-w-xl py-4">
<ThemeToggle />
<div>
<h1 className="py-2 text-xl font-bold">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Velit, magni.
</h1>
<p>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Sint enim
impedit qui omnis cumque corporis reprehenderit similique quae fugiat
magni aut suscipit ducimus vel magnam, officiis alias minus ut!
Quisquam maiores et saepe omnis magni similique quidem cum dolor unde,
dicta rerum provident illum molestiae vitae minus iure id debitis!
</p>
</div>
</div>
);
}
export default App;