从零搭建一个自定义明暗主题系统
最近准备用 React + Antd + UnoCSS 开发一个和 NestJS Admin 配套的系统,想加个自定义主题功能,效果如下图,也可以点击 这里 或 这里 体验。
theme.gif
一、需求
- 用户可以自定义主题颜色,需要实时响应;
- 用户可以切换明暗模式,需要实时改变背景和文字颜色;
- 当用户切换系统主题时,网页需要作出响应;
- 主题颜色和明暗模式需要缓存至
localStorage
。
二、准备工作
这里使用的是 pnpm,用 npm 或 yarn 等包管理工具的记得替换命令。
1. 创建项目
首先,拉取 vite 模板:
pnpm create vite my-theme --template react-ts
清空 src 目录:
image.png
启动项目:
pnpm run dev
2. 安装 Antd
pnpm add antd
image.png
3. 安装并配置 UnoCSS
开始之前,先推荐两个 VSCode 插件:
UnoCSS
unocss-ext.png这个插件会读取
uno.config.ts
,提供了类名的提示以及预览:
image.pngIconify IntelliSense
icon-ext.png这个插件提供了图标名称的提示和预览功能:
image.png
1) 安装并引入
因为后续会用的 CSS 图标,这里顺带安装一下图标库。(体积很大,70M,你想要的 SVG 图标 这里 都有)
pnpm add unocss @iconify/json -D
配置 vite.config.ts
:
在 main.tsx
中引入:
2) 配置文件
在项目根目录创建 uno.config.ts
配置文件:
更多配置选项,请阅读 UnoCSS 文档。
关于图标的使用方法和配置,请看 这里。
3) 样式重置
pnpm add @unocss/reset
在 main.tsx
中引入:
4) 测试
在 App.tsx
中随便写点代码:
image.png
三、需求实现
1. 自定义主题颜色
1) 组件引入并绑定状态
image.png
2) 和 Antd 组件同步
新版本的 Antd 采用了 CSS-in-JS 方案以及 梯度变量演变 算法,只需要提供一个基础变量 colorPrimary
,主题相关的其它配色就能推算出来,比如按钮点击的波纹颜色等等。
所以,我们只需要将 primaryColro
通过 ConfigProvider 提供给 Antd 就可以了:
antd.gif
3) 和其他颜色同步
这里使用 CSS 变量的方案来保持颜色同步:
- 给根元素添加一个 CSS 变量
--primary-color
; - 给 UnoCSS 添加一个颜色
primary: 'var(--primary-color)'
; - 添加一个副作用,让
primaryColro
和--primary-color
保持同步。
primary-color.gif
2. 明暗模块切换
安装 classnames
方便组装类名:
pnpm add classnames
1) 封装切换组件
先给图标按钮加个 shortcut 组合类:
创建组件 ToggleTheme.tsx
:
2) 引入组件
image.png
3) 绑定 dark 类
目前常用的黑暗模式方案是给根元素添加一个 dark
类,然后在代码中通过 dark:text-yellow
指定黑暗模式下的样式:
使用 useEffect
同步 dark 类:
dark-text.gif
4) 使用 CSS 变量同步颜色
新建 main.css
:
引入 main.tsx
:
效果如下:
bg-dark.gif
5) 同步 Antd
Antd 暴露的 theme
提供了几种颜色算法,我们需要用到这两种:
- defaultAlgorithm 默认算法
- darkAlgorithm 黑暗模式的算法
我们需要根据 mode
给 ConfgProvider
提供不同的算法:
效果如下:(注意看 zzz 按钮的背景颜色)
dark-antd.gif
3. 监听系统主题
刚刚我们实现了手动切换明暗模式,现在来实现根据当前的系统主题使用对应的模式。
1) 获取并监听系统主题
CSS 提供了媒体查询 prefers-color-scheme: dark
用来监听系统明暗模式,如果我们想读取,需要调用 window.matchMedia
,该方法需要传入一个查询字符串,并返回一个 MediaQueryList
对象:
- matches 布尔值
- addEventListener 添加监听事件处理函数
为了更好的逻辑封装和复用,创建一个自定义 hook usePreferredDark.ts
,返回系统是否处于黑暗模式:
测试:
效果如下:
Kapture 2024-01-29 at 16.19.04.gif
2) 结合 mode
监听系统主题我们实现了,现在需要把 preferredDark
和 mode
结合起来判断当前网页是否处于黑暗模式,封装一个自定义 hook useDark.ts
,如果是黑暗模式,返回 true:
逻辑解释:
- 因为 mode 是用户选择的,所以它优先级最高,如果 mode === 'dark',直接短路返回 true;
- 如果 mode === 'light',返回 false
- 如果 mode === 'auto',返回当前系统是否处于黑暗模式
测试:
效果如下:
Kapture 2024-01-29 at 16.36.32.gif
最后修改 mode 的初始值为 auto
:
4. 缓存至 localStorage
这个实现起来很简单,直接使用 ahooks 提供的 useLocalStorageState
即可。
1) 安装 ahooks
pnpm add ahooks
2) 替换 useState
效果如下:
Kapture 2024-01-29 at 16.48.44.gif
3) 背景闪烁
刷新页面的时候,明显可以感觉到背景颜色闪烁了一下。这是因为根元素的 dark 类是通过 JS 设置的,我们的代码会在 html 创建之后执行。
解决方案:在 index.html
的 head
中插入一段脚本:
四、总结技术要点
window.matchMedia
API- Antd
ConfigProvider
useLocalStorageState
- CSS 变量
完整代码见 GitHub。