React+TailwindCSS快速实现暗黑模式切换

使用 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.jspostcss.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
}
  1. 首先从 localStorege 中获取主题,如果没有找到,则使用默认主题。
  2. 监听主题状态的变化。每当主题改变时,更新 html 的 class。如果主题为 system,则根据 prefers-color-scheme 来检测系统的主题色设置,给 html 添加对应的 class。
  3. setTheme函数将新的主题存储在localStorage中,并更新主题状态。
  4. 通过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;
相关推荐
Mintopia2 小时前
🏗️ B端架构中的用户归因与埋点最佳实践
前端·react.js·架构
Blossom.1182 小时前
大模型AI Agent实战:ReAct框架从零实现与金融研报分析系统
人工智能·学习·react.js·stable diffusion·金融·aigc·知识图谱
南山安2 小时前
React 学习:父传子的单项数据流——props
javascript·react.js·前端框架
soda_yo2 小时前
React哲学:保持组件纯粹 哈气就要哈得纯粹
前端·react.js·设计
Bigger2 小时前
Tauri (22)——让 `Esc` 快捷键一层层退出的分层关闭方案
前端·react.js·app
T___T3 小时前
从入门到实践:React Hooks 之 useState 与 useEffect 核心解析
前端·react.js·面试
小肥宅仙女3 小时前
React + ECharts 多图表联动实战:从零实现 Tooltip 同步与锁定功能
前端·react.js·echarts
前端无涯3 小时前
React父子组件回调传参避坑指南:从基础到进阶实践
前端·react.js
白雾茫茫丶3 小时前
不只是作品集:用 Next.js 打造我的数字作品库
react.js·next.js
谢尔登3 小时前
React的Fiber架构
前端·react.js·架构