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;
相关推荐
struggle20251 小时前
AxonHub 开源程序是一个现代 AI 网关系统,提供统一的 OpenAI、Anthropic 和 AI SDK 兼容 API
css·人工智能·typescript·go·shell·powershell
需要兼职养活自己4 小时前
react【portals】与vue3【<Teleport>】
前端·react.js
用户47949283569154 小时前
React 19.2 重磅更新:终于解决 useEffect 依赖数组难题
前端·react.js
@PHARAOH4 小时前
HOW - prefetch 二级页面实践
前端·javascript·react.js
前端OnTheRun4 小时前
React18学习笔记(六) React中的类组件,极简的状态管理工具zustand,React中的Typescript
react.js·组件·
打小就很皮...5 小时前
ShowCountCard 功能迭代:新增周月对比属性,完善数据可视化场景
前端·react.js·信息可视化
执剑、天涯6 小时前
通过一个typescript的小游戏,使用单元测试实战(二)
javascript·typescript·单元测试
HHHHHY6 小时前
使用阿里lowcode,手搓一个Carousel 走马灯容器组件
前端·react.js
小芒果nana6 小时前
React入门-JSX
react.js
Komorebi゛7 小时前
【React】配置别名路径
前端·react.js·前端框架