周末无事,翻阅 Antfu 的博客,发现一篇很有意思的文章,用简单的 CSS animation 动画实现博客文章按照段落渐入,效果如下:
是不是很有意思呢?作为一名前端开发,如果产品给你提出这样的动画需求,你能否实现出来呢?在继续阅读之前,不妨先独立思考一下,如何用 CSS 来完整这种动画。
PS:什么,你问 Antfu 是谁?他可是前端圈里面的偶像级人物:
Antfu 是 Anthony Fu 的昵称,他是一位知名的开源软件开发者,活跃于前端开发社区。Anthony Fu 以其对 Vue.js 生态系统的贡献而著名,包括但不限于 Vite、VueUse 等项目。Antfu 也因为他在 GitHub 上的活跃参与和贡献而受到许多开发者的尊敬和认可。
首先用 CSS 写一个渐入动画,相信这个大家都看得懂:
css
@keyframes enter {
0% {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: none;
}
}
上述代码定义了一个名为 enter 的关键帧动画,其效果使得元素从透明度为0(完全透明)逐渐变为透明度为1(完全不透明),同时元素会在垂直方向上从 10px 以上的位置移动到最终位置。具体来说,关键帧如下:
0%
:动画的起始状态(动画开始时刻)。在这个状态中,元素的透明度opacity
设置为0,表示元素是完全透明的,看不见的。同时,transform: translateY(10px);
属性表示元素在垂直方向上被推移了10px
,即元素的起始位置是它最终位置的上方10px
。
to
或100%
:动画的结束状态(动画结束时刻)。在这个状态中,元素的透明度opacity
设置为1,表示元素完全不透明,完全可见。transform: none;
表示取消了之前的变换效果,元素恢复到它的原始形态和位置。
难道这样就行了吗?当然不行,如果仅仅对内容添加上述动画,效果是文章整体渐入,效果如下:
然而我们想要的效果是一段一段渐入呀,那怎么办呢?思路很简单:
给每个段落分别添加上述动画,然后按照先后顺序延迟播放动画。
css
[data-animate] {
--stagger: 0;
--delay: 120ms;
--start: 0ms;
animation: enter 0.6s both;
animation-delay: calc(var(--stagger) * var(--delay) + var(--start));
}
上面的关键就是 animation-delay
这个属性,为了方便 HTML 编码,这里使用了 CSS 变量来进行控制,把元素的延迟时间总结到如下的公式里面:
calc(var(--stagger) * var(--delay) + var(--start));
其中变量的含义如下:
--stagger
是段落序号,值为1、2、3...--delay
是上下两个段落的延迟时间间隔--start
是初始延迟时间,即整片文章第一段的延迟偏移量
有了这些变量,就可以按照段落的前后顺序,写出如下 HTML 代码了:
html
<p style="--stagger: 1" data-animate>Block 1</p>
<p style="--stagger: 2" data-animate>Block 2</p>
<p style="--stagger: 3" data-animate>Block 3</p>
<p style="--stagger: 4" data-animate>Block 4</p>
<p style="--stagger: 5" data-animate>Block 5</p>
<p style="--stagger: 6" data-animate>Block 6</p>
<p style="--stagger: 7" data-animate>Block 7</p>
<p style="--stagger: 8" data-animate>Block 8</p>
实现的效果如下:
可以说相当棒了!但是这里还有个问题,就是 markdown 文章转成 HTML 的时候,不会总是 p
标签吧,也有可能是 div
和 pre
等其他标签,而且你还要手动给这些标签添加 --stagger
变量,这个简直不能忍啊。Antfu 最后给出的解决方案是这样的:
css
slide-enter-content > * {
--stagger: 0;
--delay: 150ms;
--start: 0ms;
animation: slide-enter 1s both 1;
animation-delay: calc(var(--start) + var(--stagger) * var(--delay));
}
.slide-enter-content > *:nth-child(1) { --stagger: 1; }
.slide-enter-content > *:nth-child(2) { --stagger: 2; }
.slide-enter-content > *:nth-child(3) { --stagger: 3; }
.slide-enter-content > *:nth-child(4) { --stagger: 4; }
.slide-enter-content > *:nth-child(5) { --stagger: 5; }
.slide-enter-content > *:nth-child(6) { --stagger: 6; }
.slide-enter-content > *:nth-child(7) { --stagger: 7; }
.slide-enter-content > *:nth-child(8) { --stagger: 8; }
.slide-enter-content > *:nth-child(9) { --stagger: 9; }
.slide-enter-content > *:nth-child(10) { --stagger: 10; }
.slide-enter-content > *:nth-child(11) { --stagger: 11; }
.slide-enter-content > *:nth-child(12) { --stagger: 12; }
.slide-enter-content > *:nth-child(13) { --stagger: 13; }
.slide-enter-content > *:nth-child(14) { --stagger: 14; }
.slide-enter-content > *:nth-child(15) { --stagger: 15; }
.slide-enter-content > *:nth-child(16) { --stagger: 16; }
.slide-enter-content > *:nth-child(17) { --stagger: 17; }
.slide-enter-content > *:nth-child(18) { --stagger: 18; }
.slide-enter-content > *:nth-child(19) { --stagger: 19; }
.slide-enter-content > *:nth-child(20) { --stagger: 20; }
只要给文章容器增加 slide-enter-content
样式,那么通过 nth-child()
就能为其直接子元素按照顺序设置 stagger
变量啦!
秒啊,实在是妙!不得不佩服大佬的脑洞,不过,杠精的你可能会说,我的文章又不止 20 个子元素,超过 20 怎么办呢?我说哥,你不会自己往后加嘛!
感兴趣的同学可以查看最终的样式代码,跟上述 demo 有一点点区别,相信你能从中学到不少东西,例如 Antfu 把 data-animate
属性关联的样式拆成了两段:
css
[data-animate] {
--stagger: 0;
--delay: 120ms;
--start: 0ms;
}
@media (prefers-reduced-motion: no-preference) {
[data-animate] {
animation: enter 0.6s both;
animation-delay: calc(var(--stagger) * var(--delay) + var(--start));
}
}
写前端这么多年,我是第一次见到 @media (prefers-reduced-motion: no-preference)
这个媒体查询的用法,一脸懵逼,赶紧恶补了一把才知道:
在 CSS 中,@media 规则用于包含针对不同媒体类型或设备条件的样式。
prefers-reduced-motion
是一个媒体查询的功能,该功能用于检测用户是否有减少动画和动态效果的偏好。一些用户可能对屏幕上的快速或复杂动作敏感,这可能会导致不适或干扰体验,因此他们在操作系统中设置了减少动画的选项。
因此,对于那些讨厌动画的用户,就不用展示这么花哨的效果,直接展示文章就行啦!