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;
相关推荐
小程故事多_8016 小时前
拆解SOP+ReAct对话Agent,告别硬编码,解锁智能对话的柔性迭代能力
前端·人工智能·react.js·前端框架
皮卡穆18 小时前
React 中使用 react-org-tree 实现组织结构树
前端·react.js·前端框架
用户09340777351418 小时前
HarmonyOS WPS Open SDK 实践:用能力分层重构 OpenFileRequest
typescript·harmonyos
asdzx6719 小时前
使用 Spire.WordJS 在 React 中实现在线 Word 文档编辑
前端·react.js·word
kyriewen1 天前
AI 写的 React 组件,合并前必查的 6 个坏味道——每个都有代码对比
前端·javascript·react.js
光影少年1 天前
RN适配方案:屏幕适配、分辨率适配、刘海屏/全面屏适配
react native·react.js·掘金·金石计划
Charonmomo1 天前
react/antd - 不能正确打开标签页问题记录
前端·react.js·前端框架
gis开发之家2 天前
vue3直击架构痛点 依赖倒置与接口隔离——打造坚如磐石的插件与高阶组件(HOC)模式
javascript·架构·typescript·前端框架·vue3
用户600071819102 天前
【翻译】React Compiler 详解---memo
react.js
冬枳2 天前
基于OpenSeagragon封装的前端病理切片展示组件
前端·typescript