第24章 昼夜模式与毛玻璃效果
现代 UI 特效实现
作者:zyyc-0336 | 日期:2026-06-12
作者:zyyc-0336 | 日期:2026-06-12
内容标签:#CSS #昼夜模式 #毛玻璃 #前端
本文是前端学习路径第24章(阶段五:外围生态)
一句话总结:用 CSS 变量 + JS 切换主题实现昼夜模式,用 backdrop-filter: blur() 实现毛玻璃效果,两者结合就是现代网站的"高级感"密码。
学习路径建议
路径CSS 变量(自定义属性)基础 ↓ 昼夜模式原理:亮色 / 暗色两套变量 ↓ 手动实现主题切换按钮 ↓ next-themes 库(偷懒神器) ↓ backdrop-filter 毛玻璃效果 ↓ 实战:导航栏毛玻璃 + 主题切换
常见错误提示
忘加 transition:切换主题时颜色突变很刺眼,记得加过渡动画
backdrop-filter 不生效:元素必须有半透明背景(如 rgba),否则看不出效果
SSR 闪烁:服务端渲染时还没读到主题,会先闪一下亮色再变暗色,用 next-themes 解决
prefers-color-scheme 搞混:这是系统设置,不是你的主题设置,别忘了让用户手动选
目录
- 24.1 什么是昼夜模式
- 24.2 CSS 变量实现主题切换
- 24.3 手动实现主题切换按钮
- 24.4 用 next-themes 偷懒
- 24.5 backdrop-filter 毛玻璃效果
- 24.6 实战:导航栏毛玻璃 + 主题切换
- 24.7 本章复盘
24.1 什么是昼夜模式
概念解释
昼夜模式就是网站有"亮色主题"和"暗色主题"两套配色方案,用户可以一键切换。你天天用的微信、手机设置、VS Code 都有这个功能。
为什么需要?
- 晚上看亮屏刺眼,暗色模式保护眼睛
- 现代网站标配,显得专业
- 系统级暗色模式已经普及,网站应该跟着适配
实现原理
| 步骤 | 做什么 | 用什么 |
|---|---|---|
| 1 | 定义两套颜色变量 | CSS 变量(--bg-color 等) |
| 2 | 根据主题切换变量值 | 给 html 加 data-theme="dark" 类名 |
| 3 | 提供切换按钮 | JS 监听点击,切换类名 |
| 4 | 记住用户选择 | localStorage |
"我直接写两套 CSS 文件不行吗?" 可以,但维护两套文件太痛苦。用 CSS 变量只需要改变量值,样式代码只写一份。
24.2 CSS 变量实现主题切换
概念解释
CSS 变量(也叫"自定义属性")就是你可以自己定义的"颜色名字",然后在整个网站里引用它。切换主题时只需要改这个"名字"对应的值。
语法表格
| 语法 | 说明 | 示例 |
|---|---|---|
--变量名: 值; | 定义变量 | --bg-color: #ffffff; |
var(--变量名) | 使用变量 | background: var(--bg-color); |
:root { } | 全局定义(相当于 html) | 变量放这里,全站可用 |
[data-theme="dark"] | 属性选择器选中暗色主题 | 暗色变量放这里 |
完整可运行代码
css/* styles/theme.css */ /* ========== 亮色主题(默认) ========== */ :root { --bg-color: #ffffff; --text-color: #1a1a1a; --card-bg: #f5f5f5; --border-color: #e0e0e0; --primary-color: #3b82f6; --shadow-color: rgba(0, 0, 0, 0.1); } /* ========== 暗色主题 ========== */ [data-theme="dark"] { --bg-color: #1a1a2e; --text-color: #e0e0e0; --card-bg: #16213e; --border-color: #2a2a4a; --primary-color: #60a5fa; --shadow-color: rgba(0, 0, 0, 0.3); } /* ========== 应用变量 ========== */ body { background-color: var(--bg-color); color: var(--text-color); transition: background-color 0.3s ease, color 0.3s ease; } .card { background: var(--card-bg); border: 1px solid var(--border-color); box-shadow: 0 2px 8px var(--shadow-color); }
逐行讲解
| 行 | 代码 | 说明 |
|---|---|---|
| 1-7 | :root { --bg-color: #fff... } | 亮色主题的颜色定义,:root = 全局 |
| 10-16 | [data-theme="dark"] { ... } | 当 html 标签有 data-theme="dark" 时,覆盖这些变量 |
| 19-22 | body { background: var(--bg-color)... } | 使用变量,切换主题时自动变色 |
| 21 | transition: ... 0.3s ease | 加过渡动画,切换时颜色渐变不突兀 |
transition 一定要加! 不加的话主题切换是"闪"过去的,很丑。加个 0.3 秒的过渡就丝滑了。
24.3 手动实现主题切换按钮
概念解释
手动实现就是不用任何库,纯用原生 JS 来切换主题。原理很简单:点击按钮 → 切换 html 的 data-theme 属性 → CSS 变量自动生效。
完整可运行代码
jsx// components/ThemeToggle.jsx 'use client' import { useState, useEffect } from 'react' export default function ThemeToggle() { const [theme, setTheme] = useState('light') // 页面加载时,读取 localStorage 里保存的主题 useEffect(() => { const saved = localStorage.getItem('theme') if (saved) { setTheme(saved) document.documentElement.setAttribute('data-theme', saved) } }, []) // 切换主题 const toggleTheme = () => { const newTheme = theme === 'light' ? 'dark' : 'light' setTheme(newTheme) document.documentElement.setAttribute('data-theme', newTheme) localStorage.setItem('theme', newTheme) // 记住选择 } return ( <button onClick={toggleTheme} className="theme-toggle"> {theme === 'light' ? '🌙 切换暗色' : '☀️ 切换亮色'} </button> ) }
css/* 按钮样式 */ .theme-toggle { padding: 8px 16px; border: 2px solid var(--border-color); border-radius: 20px; background: var(--card-bg); color: var(--text-color); cursor: pointer; font-size: 14px; transition: all 0.3s ease; } .theme-toggle:hover { border-color: var(--primary-color); }
逐行讲解
| 行 | 代码 | 说明 |
|---|---|---|
| 5 | const [theme, setTheme] = useState('light') | 用 state 记录当前主题 |
| 8-13 | useEffect(() => {...}) | 页面加载时从 localStorage 读取上次保存的主题 |
| 10 | document.documentElement.setAttribute(...) | 给 <html> 标签设置 data-theme 属性 |
| 17-21 | toggleTheme 函数 | 亮变暗、暗变亮,同时存到 localStorage |
| 25 | {theme === 'light' ? '🌙' : '☀️'} | 根据主题显示不同图标 |
document.documentElement 是什么? 就是 <html> 标签。不是 document.body!变量是定义在 :root(等于 html)上的,所以要改 html 的属性。
忘记 'use client':这个组件用了 useState 和 useEffect,是客户端组件,必须加这行,Next.js 才不会报错。
24.4 用 next-themes 偷懒
概念解释
next-themes 是专门为 Next.js 写的主题切换库,帮你处理了:
- SSR 时的主题闪烁问题
- localStorage 读写
- 系统主题检测(自动跟随系统暗色模式)
- 初始加载时的 FOUC(无样式内容闪烁)
一句话:5 行代码搞定主题切换,别自己造轮子了。
安装与使用对比
| 对比项 | 手动实现 | next-themes |
|---|---|---|
| 代码量 | 30+ 行 | 5 行 |
| SSR 闪烁 | 需要自己处理 | 自动处理 |
| 系统主题跟随 | 要自己写 | 内置支持 |
| localStorage | 手动读写 | 自动 |
| 学习成本 | 低(但坑多) | 极低 |
完整可运行代码
bash# 第 1 步:安装 npm install next-themes
jsx// app/providers.jsx 'use client' import { ThemeProvider } from 'next-themes' export default function Providers({ children }) { return ( <ThemeProvider attribute="data-theme" defaultTheme="system"> {children} </ThemeProvider> ) }
jsx// app/layout.jsx import Providers from './providers' export default function RootLayout({ children }) { return ( <html lang="zh" suppressHydrationWarning> <body> <Providers> {children} </Providers> </body> </html> ) }
jsx// components/ThemeToggle.jsx 'use client' import { useTheme } from 'next-themes' import { useEffect, useState } from 'react' export default function ThemeToggle() { const { theme, setTheme } = useTheme() const [mounted, setMounted] = useState(false) // 避免 SSR 水合不匹配 useEffect(() => setMounted(true), []) if (!mounted) return null return ( <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="theme-toggle" > {theme === 'dark' ? '☀️ 亮色模式' : '🌙 暗色模式'} </button> ) }
逐行讲解
| 文件 | 关键代码 | 说明 |
|---|---|---|
| providers.jsx | <ThemeProvider attribute="data-theme"> | 告诉库用 data-theme 属性切换主题 |
| providers.jsx | defaultTheme="system" | 默认跟随系统设置 |
| layout.jsx | suppressHydrationWarning | 防止 SSR 水合警告(因为服务端和客户端主题可能不同) |
| ThemeToggle.jsx | const { theme, setTheme } = useTheme() | 一行代码获取主题状态和切换函数 |
| ThemeToggle.jsx | if (!mounted) return null | 服务端不渲染,避免闪烁 |
mounted 是干嘛的? 服务端渲染时不知道用户是什么主题,直接渲染会导致服务端和客户端内容不一致(水合错误)。先返回 null,等客户端挂载后再渲染。
suppressHydrationWarning 别乱加:只在 <html> 标签上加,其他地方加了会掩盖真正的错误。
24.5 backdrop-filter 毛玻璃效果
概念解释
毛玻璃效果就是让元素背景变成"磨砂玻璃"——你能透过它看到后面的模糊内容,但又看不清楚。macOS 的 Dock 栏、iOS 的通知中心都是这种效果。
语法表格
| 属性 | 值 | 说明 |
|---|---|---|
backdrop-filter: blur(10px) | 数值越大越模糊 | 核心属性,设置模糊半径 |
backdrop-filter: blur() saturate(180%) | 多个滤镜组合 | 模糊 + 增强饱和度 |
background: rgba(255,255,255,0.7) | 半透明背景 | 必须有,否则看不出模糊 |
-webkit-backdrop-filter | 同上 | Safari 浏览器需要加前缀 |
关键:为什么必须有半透明背景?
| 背景设置 | 效果 |
|---|---|
background: white | 完全看不到毛玻璃,白色挡住了 |
background: rgba(255,255,255,0.7) | ✅ 70% 白色 + 透过去的内容模糊 |
background: rgba(255,255,255,0) | 全透明,模糊效果太强,内容看不清 |
| 没写 background | ❌ 无效,backdrop-filter 需要背景才能生效 |
完整可运行代码
css/* 毛玻璃卡片 */ .glass-card { background: rgba(255, 255, 255, 0.25); /* 半透明背景 */ backdrop-filter: blur(12px); /* 核心:模糊 */ -webkit-backdrop-filter: blur(12px); /* Safari 兼容 */ border: 1px solid rgba(255, 255, 255, 0.3); /* 半透明边框 */ border-radius: 12px; padding: 20px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); } /* 暗色模式下的毛玻璃 */ [data-theme="dark"] .glass-card { background: rgba(0, 0, 0, 0.25); /* 暗色半透明 */ border: 1px solid rgba(255, 255, 255, 0.1); }
逐行讲解
| 行 | 代码 | 说明 |
|---|---|---|
| 2 | background: rgba(255, 255, 255, 0.25) | 25% 不透明度的白色,让后面的内容可以"透过来" |
| 3 | backdrop-filter: blur(12px) | 对元素后面的内容施加 12px 高斯模糊 |
| 4 | -webkit-backdrop-filter | Safari 浏览器的兼容写法,必须加 |
| 5 | border: 1px solid rgba(255,255,255,0.3) | 半透明边框,和毛玻璃风格统一 |
写了 backdrop-filter 没效果? 99% 是因为没有写 background(或背景完全不透明)。毛玻璃效果的本质是"透过半透明背景看模糊的后面内容",没有半透明背景就没效果。
Safari 没效果? 忘了加 -webkit-backdrop-filter。苹果系浏览器(Safari、iOS WebView)必须加 -webkit- 前缀。
filter: blur() ≠ backdrop-filter: blur():前者模糊的是元素自身内容,后者模糊的是元素后面的内容。导航栏要用 backdrop-filter!
24.6 实战:导航栏毛玻璃 + 主题切换
完整可运行代码
jsx// components/Navbar.jsx 'use client' import { useTheme } from 'next-themes' import { useEffect, useState } from 'react' export default function Navbar() { const { theme, setTheme } = useTheme() const [mounted, setMounted] = useState(false) useEffect(() => setMounted(true), []) return ( <nav className="navbar"> <div className="navbar-inner"> {/* 左侧 Logo */} <span className="logo">🌐 MyBlog</span> {/* 右侧导航 + 主题切换 */} <div className="nav-right"> <a href="/">首页</a> <a href="/about">关于</a> <a href="/blog">文章</a> {mounted && ( <button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="theme-btn" > {theme === 'dark' ? '☀️' : '🌙'} </button> )} </div> </div> </nav> ) }
css/* styles/navbar.css */ /* 导航栏:固定在顶部 + 毛玻璃 */ .navbar { position: fixed; /* 固定定位,滚动时跟着走 */ top: 0; left: 0; right: 0; z-index: 1000; /* 确保在最上层 */ /* 毛玻璃三件套 */ background: rgba(255, 255, 255, 0.7); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border-bottom: 1px solid rgba(0, 0, 0, 0.1); transition: background 0.3s ease; } /* 暗色模式导航栏 */ [data-theme="dark"] .navbar { background: rgba(26, 26, 46, 0.7); border-bottom: 1px solid rgba(255, 255, 255, 0.05); } /* 导航栏内容居中 */ .navbar-inner { max-width: 1200px; margin: 0 auto; padding: 12px 24px; display: flex; justify-content: space-between; align-items: center; } /* Logo */ .logo { font-size: 20px; font-weight: bold; color: var(--text-color); } /* 右侧导航链接 */ .nav-right { display: flex; align-items: center; gap: 20px; } .nav-right a { color: var(--text-color); text-decoration: none; font-size: 15px; opacity: 0.8; transition: opacity 0.2s; } .nav-right a:hover { opacity: 1; } /* 主题切换按钮 */ .theme-btn { padding: 6px 10px; border: 1px solid var(--border-color); border-radius: 8px; background: transparent; cursor: pointer; font-size: 18px; transition: all 0.2s; } .theme-btn:hover { background: var(--card-bg); }
效果说明
| 特性 | 实现方式 |
|---|---|
| 导航栏固定在顶部 | position: fixed |
| 背景模糊(毛玻璃) | backdrop-filter: blur(16px) |
| 半透明背景 | rgba(255,255,255,0.7) |
| 暗色模式适配 | [data-theme="dark"] 选择器覆盖颜色 |
| 切换主题 | next-themes 的 useTheme |
| 防止闪烁 | mounted 状态判断 |
position: fixed 后导航栏"消失"了? 可能被其他元素挡住了,检查 z-index 是否足够大。
max-width: 1200px 的作用? 让内容不会在大屏幕上拉得太宽。margin: 0 auto 让它居中。
24.7 本章复盘
| 知识点 | 核心内容 | 一句话记忆 |
|---|---|---|
| CSS 变量 | --变量名 定义,var() 使用 | 变量是主题切换的基础 |
| 主题切换原理 | 给 html 加 data-theme 属性,CSS 变量自动切换 | 改一个属性,全局颜色跟着变 |
| 手动实现 | JS 切换 data-theme + localStorage 持久化 | 自己造轮子,30 行代码 |
| next-themes | ThemeProvider 包裹 + useTheme 使用 | 5 行代码,自动处理 SSR 闪烁 |
| mounted 防闪烁 | useEffect 设置 mounted,未挂载返回 null | 服务端不渲染主题相关内容 |
| backdrop-filter | blur(12px) 模糊后面的内容 | 必须配合半透明 background |
| -webkit- 前缀 | Safari 浏览器兼容 | 苹果浏览器要加前缀 |
| filter vs backdrop-filter | filter 模糊自身,backdrop-filter 模糊后面 | 导航栏用 backdrop |
| suppressHydrationWarning | 只在 <html> 上加,消除 SSR 警告 | 别乱加,只加 html 标签 |
速查卡片
速查🎨 昼夜模式 = CSS 变量 + data-theme 切换 🧊 毛玻璃 = 半透明背景 + backdrop-filter: blur() ⚡ next-themes = ThemeProvider + useTheme 🔒 持久化 = localStorage(库帮你搞定) 🚫 防闪烁 = mounted 状态 + suppressHydrationWarning
🎉 恭喜你完成了阶段五(外围生态)的全部学习! 从 Tailwind CSS 到第三方组件,从 MongoDB 数据库到评论功能,再到昼夜模式与毛玻璃效果——你的前端技能树又点亮了一大片。
下一章预告:前端基础学习告一段落!接下来可以探索TypeScript、状态管理、测试等进阶主题。
本文档由 zyyc-0336 编写,最后更新:2026-06-14