react【实战】首页 -- 白天/黑夜主题切换(含组件封装)

最终效果

组件封装

技术栈: react19 + Tailwind CSS

src/components/darkToggle.jsx

ts 复制代码
import { FiSun } from "react-icons/fi";
import { FaRegMoon, FaMoon } from "react-icons/fa";
import { useState, useEffect } from "react";

function DarkToggle() {
  const [isDark, setIsDark] = useState(() => {
    const savedTheme = localStorage.getItem("theme");
    const hasDarkClass = document.documentElement.classList.contains("dark");
    return savedTheme === "dark" || (!savedTheme && hasDarkClass);
  });

  useEffect(() => {
    if (isDark) {
      document.documentElement.classList.add("dark");
    } else {
      document.documentElement.classList.remove("dark");
    }
  }, [isDark]);

  const toggleDark = () => {
    const newDark = !isDark;
    setIsDark(newDark);
    localStorage.setItem("theme", newDark ? "dark" : "light");
  };

  return (
    <button
      className="p-2 rounded-full cursor-pointer transition-all duration-300 group"
      onClick={toggleDark}
      aria-label="切换深色模式"
    >
      {isDark ? (
        <FiSun
          size={20}
          className="text-amber-400 transition-all duration-300 hover:rotate-90"
        />
      ) : (
        <div className="relative">
          <FaRegMoon
            size={20}
            className="text-gray-700 dark:text-gray-300 transition-all duration-150 group-hover:opacity-0"
          />
          <FaMoon
            size={20}
            className="text-black dark:text-white absolute top-0 left-0 transition-all duration-150 opacity-0 group-hover:opacity-100 group-hover:scale-90"
          />
        </div>
      )}
    </button>
  );
}

export default DarkToggle;

使用

1. 配置 Tailwind CSS,让其支持手动切换主题

src/index.css

在全局样式文件中,添加下方代码

css 复制代码
@import "tailwindcss";
/* 当根元素有 class="dark" 时 → 激活 dark: 样式,用于在页面上切换主题为暗黑模式 */
@custom-variant dark (&:where(.dark, .dark *));

2. 导入渲染组件

ts 复制代码
import DarkToggle from "./DarkToggle";
html 复制代码
<DarkToggle />

3. 给页面添加黑夜的主题样式

以导航 nav 为例,添加了黑夜的主题样式

css 复制代码
dark:bg-black/90

图标 logo 添加了黑夜的主题样式

c 复制代码
dark:fill-white

特别注意:因范例中 logo 使用的 svg 图标,需用 fill- 的语法来填充颜色

范例的完整代码如下:

ts 复制代码
import Logo from "../assets/apple.svg?react";
import { AiOutlineMenu, AiOutlineSearch } from "react-icons/ai";
import { useState } from "react";
import DarkToggle from "./DarkToggle";
const Header = () => {
  const [isOpen, setIsOpen] = useState(false);
  const [isSearchEnable, setIsSearchEnable] = useState(false);
  const [searchValue, setSearchValue] = useState("");

  return (
    <nav className="flex gap-2 items-center justify-between px-4 h-16 shadow-md sticky top-0 z-50 bg-white/70 backdrop-blur-md dark:bg-black/90 transition-all duration-100 ">
      <a href="#" className="text-xl font-bold">
        <Logo className="w-6 h-6 hover:scale-105 transition-transform  dark:fill-white" />
      </a>

      <div className="gap-6 hidden md:flex mx-auto dark:text-white">
        <a href="#">商店</a>
        <a href="#">电脑</a>
        <a href="#">手机</a>
        <a href="#">智能家居</a>
        <a href="#">娱乐</a>
        <a href="#">技术支持</a>
      </div>
      {isSearchEnable && (
        <div className="relative">
          <input
            id="search-input"
            className="peer border border-gray-300 px-4 py-2 w-64 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 transition dark:border-gray-700 dark:bg-black/90 dark:text-white"
            placeholder=" "
            value={searchValue}
            onChange={(e) => setSearchValue(e.target.value)}
          />
          <label
            htmlFor="search-input"
            className="absolute left-2 -top-2 bg-white px-2 text-xs text-gray-500 transition-all duration-100 cursor-pointer
                    peer-placeholder-shown:top-2 
                    peer-placeholder-shown:text-sm
                    peer-focus:-top-2 
                      peer-focus:left-2 
                    peer-focus:text-blue-500
                    peer-focus:text-xs
                    dark:text-white
                    dark:bg-black/90
                    "
          >
            搜索
          </label>
          {searchValue && (
            <button
              onClick={() => setSearchValue("")}
              className="absolute right-4 top-2 text-gray-400 hover:text-gray-600 cursor-pointer peer-focus:text-blue-500"
            >
              ✕
            </button>
          )}
        </div>
      )}
      <div className="flex gap-2 ml  dark:text-white ">
        <button
          onClick={() => setIsSearchEnable(!isSearchEnable)}
          className="cursor-pointer"
        >
          <AiOutlineSearch size={24} />
        </button>
        <DarkToggle />
        <button
          className="md:hidden cursor-pointer"
          onClick={() => setIsOpen(true)}
        >
          <AiOutlineMenu size={24} />
        </button>
      </div>
      <div
        className={`md:hidden fixed top-0 right-0 h-full w-64
        ${!isOpen && "hidden"}
        `}
      >
        <div className="flex flex-col mt-17 space-y-6 bg-white text-center p-6 rounded-lg">
          <a href="#">商店</a>
          <a href="#">电脑</a>
          <a href="#">手机</a>
          <a href="#">智能家居</a>
          <a href="#">娱乐</a>
          <a href="#">技术支持</a>
        </div>
      </div>
      {/* 移动端,菜单展开时,导航栏添加模糊遮罩 */}
      {isOpen && (
        <div
          className="fixed inset-0 bg-black/50 backdrop-blur-md"
          onClick={() => setIsOpen(false)}
        ></div>
      )}
    </nav>
  );
};
export default Header;
相关推荐
子兮曰1 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰1 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万1 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝1 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋1 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁1 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95271 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大1 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师1 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学1 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端