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;
相关推荐
前端老宋Running40 分钟前
“受控组件”的诅咒:为什么你需要 React Hook Form + Zod 来拯救你的键盘?
前端·javascript·react.js
风止何安啊40 分钟前
拿捏 React 组件通讯:从父子到跨组件的「传功秘籍」
前端·react.js·面试
韭菜炒大葱1 小时前
React 新手村通关指南:状态、组件与魔法 UI 🧙‍♂️
前端·javascript·react.js
今天不要写bug3 小时前
基于qrcode前端实现链接转二维码的生成与下载
前端·javascript·typescript·vue
憧憬少6 小时前
记录一个Typescript的IoC容器的实现
typescript
用户12039112947268 小时前
从零掌握 React JSX:为什么它让前端开发像搭积木一样简单?
前端·react.js·面试
行走的陀螺仪20 小时前
高级前端 Input 公共组件设计方案(Vue3 + TypeScript)
前端·javascript·typescript·vue·组件设计方案
by__csdn1 天前
微前端架构:从理论到实践的全面解析
前端·javascript·vue.js·架构·typescript·vue·ecmascript
3秒一个大1 天前
JSX 基本语法与 React 组件化思想
前端·react.js
HexCIer1 天前
Arco Design 停摆!字节跳动 UI 库凉了?
react.js·前端框架