一. 切换主题动画
上一篇文章,我写了仅用一行代码实现全网站暗黑模式,引起一些不错的反响,其中有倔友提出,Element Plus 官网的那种切换效果是如何实现的?
例如:
Element Plus 官网的主题切换效果:
Vben Admin 官方示例的主题切换效果:
可见,这种动画效果在切换主题时是比较常见的,同时也是比较炫酷的,于是本篇文章我们使用简单的方案来实现一下。
本篇文章实现效果如下,文末附完整源码!
二. 核心代码实现
以上的主题切换动画效果,主要依靠现代 CSS 和 JavaScript 的 View Transition API 技术。
- 点击位置动画:动画从点击位置开始扩散,覆盖整个页面
- 平滑过渡效果:使用 CSS 过渡确保颜色变化平滑
点击切换后一般会经历以下流程,我们来模拟一下:
点击按钮
↓
触发 toggleTheme 函数
↓
启动 View Transition
↓
切换 dark 类名
↓
过渡准备就绪
↓
计算点击位置和扩散半径
↓
创建圆形裁剪路径动画
↓
根据当前主题状态决定动画方向
↓
执行动画效果
↓
完成主题切换
核心代码如下:
2.1 toggleTheme
函数
toggoleTheme
主要实现点击按钮时的主题切换动画:
function toggleTheme(event) {
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
transition.ready.then(() => {
const { clientX, clientY } = event
const radius = Math.hypot(Math.max(clientX, innerWidth - clientX), Math.max(clientY, innerHeight - clientY))
const clipPath = [`circle(0px at ${clientX}px ${clientY}px)`, `circle(${radius}px at ${clientX}px ${clientY}px)`]
const isDark = document.documentElement.classList.contains('dark')
document.documentElement.animate(
{
clipPath: isDark ? clipPath.reverse() : clipPath
},
{
duration: 450,
easing: 'ease-in',
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)'
}
)
})
}
2.2 CSS 样式支持
CSS 为 View Transition API 提供必要的样式支持,确保主题切换动画能正确显示。
/* 1. 重置基础动画和混合模式 */
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
/* 2. 亮色主题时的层级控制 */
::view-transition-old(root) {
z-index: 1;
}
::view-transition-new(root) {
z-index: 2147483646;
}
/* 3. 暗色主题时的层级反转 */
html.dark::view-transition-old(root) {
z-index: 2147483646;
}
html.dark::view-transition-new(root) {
z-index: 1;
}
以上两个就是实现动画的核心代码,仅需要一个 toggleTheme
函数和几行 CSS
样式支持,接下来我们来逐步分析一下。
三. toggleTheme 实现流程
3.1 启动视图过渡
使用 View Transition API
开始主题切换动画,使用 document.startViewTransition()
创建平滑的过渡效果:
const transition = document.startViewTransition(() => {
document.documentElement.classList.toggle('dark')
})
-
document.startViewTransition()
:- 这是
View Transition API
的核心方法,它允许你在 DOM 更新时创建平滑的过渡动画 - 返回一个
transition
对象,可以用来控制过渡过程
- 这是
-
原理:
-
这是主题切换动画的启动器
-
当执行这段代码时,浏览器会:
- 捕获当前页面的快照(旧状态)
- 执行回调函数中的 DOM 更新
- 捕获更新后的页面快照(新状态)
- 在这两个状态之间创建平滑的过渡动画
-
可以通过
transition.ready
和transition.finished
Promise 来添加自定义动画效果 -
圆形扩散动画就是在
transition.ready.then()
中实现的
-
3.2 执行过渡动画
当过渡准备就绪后,执行自定义动画效果,实现细节如下:
获取点击位置坐标
获取用户点击的坐标位置 (clientX, clientY)
// 获取点击位置坐标
const { clientX, clientY } = event
计算扩散半径
计算从点击位置到屏幕边缘的最大距离作为动画结束半径
// 计算圆形扩散半径
const radius = Math.hypot(
Math.max(clientX, innerWidth - clientX),
Math.max(clientY, innerHeight - clientY)
)
计算从用户点击位置到屏幕边缘的最大距离,将其作为圆形扩散动画的结束半径。
Math.max(clientX, innerWidth - clientX)
:计算点击位置的横坐标clientX
到屏幕左右边缘的最大距离。Math.max(clientY, innerHeight - clientY)
:计算点击位置的纵坐标clientY
到屏幕上下边缘的最大距离。Math.hypot()
:使用勾股定理计算上述两个最大距离构成的直角三角形的斜边长度,即从点击位置到屏幕边缘的最大直线距离。- 最终得到的
radius
就是圆形扩散动画从点击点开始扩散时需要达到的半径。
裁剪路径动画
创建从点击点开始的圆形扩散动画
// 定义裁剪路径动画
const clipPath = [
circle(0px at x y), // 初始状态:半径为0的圆(不可见)
circle(radius at x y) // 结束状态:完全展开的圆
]
以上是创建了一个用于主题切换动画的 CSS 裁剪路径(clipPath)数组,它定义了两个状态:
- 第一个元素表示动画起始状态:
circle(0px at x y)
:在点击位置(x,y)创建一个半径为 0 的圆(完全不可见)
- 第二个元素表示动画结束状态:
circle(radius at x y)
:在相同位置创建一个半径为radius
的圆(覆盖整个屏幕)
- 说明:
x y
:用户点击的屏幕坐标位置radius
:预先计算出的从点击点到屏幕边缘的最大距离
执行路径动画
使用 animate()
方法执行裁剪路径动画
document.documentElement.animate(
{
clipPath: isDark ? clipPath.reverse() : clipPath // 动画属性:裁剪路径
},
{
duration: 450, // 动画持续时间450毫秒
easing: 'ease-in', // 缓动函数:开始慢,逐渐加速
pseudoElement: isDark ? '::view-transition-old(root)' : '::view-transition-new(root)' // 目标伪元素
}
)
-
动画属性对象:
clipPath
: 使用之前定义的圆形裁剪路径isDark ? clipPath.reverse() : clipPath
:- 如果是暗黑模式(
isDark=true
),则反转动画方向(从大圆缩小到小圆) - 如果是亮色模式,则保持原方向(从小圆放大到大圆)
- 如果是暗黑模式(
-
动画配置对象:
duration: 450
: 动画持续 450 毫秒easing: 'ease-in'
: 使用"缓入"效果,动画开始时较慢,然后加速pseudoElement
: 根据当前主题选择目标伪元素- 暗黑模式使用
::view-transition-old(root)
(旧视图) - 亮色模式使用
::view-transition-new(root)
(新视图)
- 暗黑模式使用
-
原理:
- 当切换主题时,浏览器会捕获当前页面的快照(旧视图)和新状态的快照(新视图)
- 这段动画会在两个视图之间创建平滑过渡
- 圆形裁剪路径动画会从点击点开始扩散或收缩,产生 Element Plus 官网那种视觉效果
四. CSS 样式支持
- 动画效果依赖于前面定义的 CSS 伪元素样式
- 通过
z-index
控制新旧视图的堆叠顺序,确保正确的视觉层次
/* 1. 重置基础动画和混合模式 */
::view-transition-old(root),
::view-transition-new(root) {
animation: none; /* 禁用默认动画 */
mix-blend-mode: normal; /* 使用正常混合模式 */
}
/* 2. 亮色主题时的层级控制 */
::view-transition-old(root) {
z-index: 1; /* 旧视图(亮色)在底层 */
}
::view-transition-new(root) {
z-index: 2147483646; /* 新视图(暗色)在顶层(最大z-index值) */
}
/* 3. 暗色主题时的层级反转 */
html.dark::view-transition-old(root) {
z-index: 2147483646; /* 旧视图(暗色)在顶层 */
}
html.dark::view-transition-new(root) {
z-index: 1; /* 新视图(亮色)在底层 */
}
说明:
-
动画控制:
animation: none
禁用浏览器默认的淡入淡出动画- 为自定义的圆形扩散动画提供干净的画布
-
层级管理:
- 使用
z-index
控制新旧视图的堆叠顺序 - 2147483647 是浏览器支持的最大 z-index 值(这里用了 2147483646)
- 确保当前主题视图始终显示在最上层
- 使用
-
主题适配:
- 通过
.dark
类选择器区分主题状态 - 动态反转新旧视图的层级关系
- 亮色 → 暗色:新视图在上层展开
- 暗色 → 亮色:旧视图在上层收缩
- 通过
-
效果优化:
mix-blend-mode: normal
防止颜色混合异常- 确保圆形裁剪动画能干净利落地覆盖整个屏幕
以上 CSS 与 animate()
方法配合,共同实现了从点击位置扩散/收缩的丝滑主题切换效果。
五. 兼容性
注意:View Transition API 属于比较新的 API,在一些浏览器中可能无法使用,需要处理一下
加入以下代码优化:
// 检查浏览器是否支持 View Transition API
if (!document.startViewTransition) {
// 不支持则直接切换主题,不添加动画
document.documentElement.classList.toggle('dark')
return
}
兼容性如下图所示:
完整代码
已发布在码上掘金,点击切换主题
可体验效果,点击右上角码上掘金
查看详情并可复制完整源码,直接使用: