最终效果

组件封装
技术栈: 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;