网页动画是能够极大地增强网站外观和感觉的因素之一。 可悲的是,与移动应用不同,使用动画来发挥其优势的网站并不像您想象的那么多。 我们不希望您的网站也成为其中之一,因此本文适合您以及任何其他正在寻找使用动画改善用户体验方法的人! 具体来说,我们将学习如何使用 CSS 动画使网页交互变得令人愉悦。
以下是我们将一起构建的内容
在我们继续之前,值得一提的是,我将假设您至少对现代前端框架有一定的了解,并且对 CSS 动画有基本的理解。 如果您不了解,也不用担心! CSS-Tricks 有关于 React 和 Vue 的出色指南,以及一篇关于 CSS animation
属性 的详尽的年鉴文章。
了解了吗? 好,让我们来谈谈我们为什么首先要使用动画,并介绍一些关于 CSS 动画的基础信息。
我们为什么要做任何动画?
我们可能仅针对此主题单独撰写一篇完整的文章。 哦,等等! Sarah Drasner 已经这么做了,她的观点既深刻又引人注目。
但是,根据我自己的经验总结一下
- 动画增强了用户与界面的交互方式。 例如,智能动画可以通过为用户提供页面转换之间更好的上下文来减少认知负荷。
- 它们可以为用户提供清晰的提示,例如我们希望他们关注的位置。
- 动画本身就是另一种设计模式,帮助用户对界面产生情感依恋并参与其中。
- 使用动画的另一个好处是,它们可以创造出网站或应用加载速度比实际更快的感知。
动画的一些基本规则
您是否曾经遇到过一个所有东西都动画化的网站? 哇,这些可能会让人感到刺眼。 因此,在使用动画时,这里有一些需要注意的事项,以避免我们的应用落入同样的境地
- 避免对除
transform
和opacity
之外的 CSS 属性进行动画处理。 如果必须对其他属性(如宽度或高度)进行动画处理,则确保同时不会发生大量布局更改。 动画实际上是有成本的,您可以通过参考 CSS Triggers 来查看确切的成本。 - 此外,尽管动画可以创造感知的性能提升,但在使用动画时实际上存在收益递减点。 同时为过多元素设置动画可能会导致性能下降。
现在我们可以开始动手编写一些代码了!
让我们构建一个音乐应用
我们将构建之前查看过的音乐应用,它受到 Aurélien Salomon 的 Dribbble 作品 的启发。 我选择此示例是为了让我们能够专注于动画,不仅在组件内部,而且在不同路由之间。 我们将使用 Vue 构建此应用,并使用原生(即无框架)CSS 创建动画。
动画应该与 UI 开发齐头并进。 在定义动画之前创建 UI 可能会花费更多时间。 在这种情况下,Dribbble 作品为我们提供了这样的范围。
让我们开始开发。
步骤 1:在本地启动应用
首先要做的。 我们需要设置一个新的 Vue 项目。 同样,我们假设您对 Vue 有一些基础了解,因此请查看 Vue 学习指南 以获取有关设置的更多信息。
我们的工作需要几个依赖项,特别是用于在视图之间转换的vue-router
和sass-loader
,以便我们可以使用 Sass 并编译为 CSS。 这里有一个关于使用路由的详细 教程,Sass 可以通过将命令行指向项目目录并使用npm install -D sass-loader node-sass
来安装。
我们拥有所需的一切!
步骤 2:设置路由
为了创建路由,我们首先要创建两个最基本的组件——Artists.vue
和Tracks.vue
。 我们将在src
文件夹中放置一个名为router.js
的新文件,并为这些组件添加如下路由
import Vue from 'vue'
import Router from 'vue-router'
import Artists from './components/Artists.vue'
import Tracks from './components/Tracks.vue'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'artists',
component: Artists
},
{
path: '/:id',
name: 'tracks',
component: Tracks
}
]
})
将router.js
导入到main.js
中,并将其注入到 Vue 实例中。 最后,用<router-view/>
替换App.vue
的内容。
步骤 3:创建音乐应用的组件和内容
我们需要两个组件,我们将通过动画在它们之间进行转换。 它们将是
Artists.vue
:艺术家网格Tracks.vue
:艺术家图片,带有一个返回按钮
如果您想稍微提前一点,这里有一些可以使用的资源
完成后,这两个视图将显示如下

步骤 4:动画!
我们到了,这是我们一直想要达到的部分。 应用中最主要的动画是在点击艺术家时从 Artists 转换到 Tracks。 它应该感觉无缝,点击艺术家图片会使该图片成为焦点,同时从一个视图转换到下一个视图。 这正是我们在应用中很少看到的动画类型,但它可以极大地减少用户的认知负荷。
为了确保我们都在同一页面上,我们将参考序列中的第一张图片作为“前一张”图片,第二张图片作为“当前”图片。 只要我们知道转换中前一张图片的尺寸和位置,就可以轻松获得效果。 我们可以通过根据前一张图片变换当前图片来对其进行动画处理。
我使用的公式是transform: translate(x, y) scale(n)
,其中n
等于前一张图片的尺寸除以当前图片的尺寸。 请注意,由于所有图片的尺寸都是固定的,因此我们可以使用n
的静态值。 例如,Artists 视图中的图片尺寸为190x190
,Tracks 视图中的图片尺寸为240x240
。 因此,我们可以将n
替换为190/240 = 0.791
。 这意味着我们的等式中变换值变为translate(x, y) scale(0.791)
。

接下来是找到x
和y
。 我们可以通过 Artists 视图中的点击事件获取这些值,如下所示
const {x, y} = event.target.getBoundingClientRect()
…然后将这些值发送到 Tracks 视图,同时切换路由。由于我们没有使用任何状态管理库,这两个组件将通过它们的父组件进行通信,父组件是顶级组件,即 App.vue
。在 App.vue
中,让我们创建一个方法来切换路由并将图像信息作为参数发送。
gotoTracks(position, artistId) {
this.$router.push({
name: 'tracks',
params: {
id: artistId,
position: position
}
})
}
如果你感兴趣,可以参考仓库中的相关代码。
既然我们在 Tracks 中已经收到了图像的位置和 ID,我们就拥有了显示和动画所需的所有数据。我们首先使用艺术家 ID 获取艺术家信息(特别是姓名和图像 URL)。
要为图像添加动画,我们需要根据图像的起始位置计算 transform
值。为了设置 transform
值,我使用了 CSS 自定义属性,也可以使用 CSS-in-JS 技术来实现。请注意,我们通过 props 收到的图像位置相对于窗口。因此,我们必须减去容器 <div>
内边距引起的固定偏移量,以使我们的计算结果一致。
const { x, y } = this.$route.params.position
// padding-left
const offsetLeft = 100
// padding-top
const offsetTop = 30
// Set CSS custom property value
document.documentElement.style.setProperty(
'--translate',
`translate(${x - offsetLeft}px, ${y - offsetTop}px) scale(0.792)`
)
我们将使用此值创建一个关键帧动画来移动图像。
@keyframes move-image {
from {
transform: var(--translate);
}
}
这将分配给 CSS 动画。
.image {
animation: move-image 0.6s;
}
…它将在组件加载时将图像从此 transform 值动画到其原始位置。

当我们向相反方向(从 Tracks 到 Artists)切换时,可以使用相同的技巧。由于我们已经在父组件中存储了被点击图像的位置,因此我们也可以将其作为 props 传递给 Artists。

步骤 5:显示曲目!
我们现在可以无缝地在两个视图之间切换,这很棒,但 Tracks 视图目前非常简陋。所以让我们为选定的艺术家添加曲目列表。
我们将创建一个空白的白框和一个新的关键帧,以便在页面加载时将其向上滑动。然后我们将向其中添加三个子部分:最近曲目、热门曲目和播放列表。同样,如果你想提前了解,可以随意参考或从仓库中复制最终代码。

最近曲目是艺术家图像正下方的缩略图行,每个缩略图下方都包含曲目名称和曲目时长。由于我们这里介绍的是动画,因此我们将创建一个向上缩放的动画,其中图像一开始是不可见的 (opacity: 0
) 并且比其自然大小略小 (scale(0.7)
),然后显示 (opacity: 1
) 并缩放至其自然大小 (transform: none
)。
.track {
opacity: 0;
transform: scale(0.7);
animation: scale-up 1s ease forwards;
}
@keyframes scale-up {
to {
opacity: 1;
transform: none;
}
}
热门曲目列表和播放列表并排位于最近曲目下方,其中热门曲目占据了大部分空间。我们可以使用另一组关键帧在初始视图中稍微向上滑动它们。
.track {
...
animation: slide-up 1.5s;
}
@keyframes slide-up {
from {
transform: translateY(140px);
}
}
为了使动画感觉更自然,我们将通过为每个项目添加递增的动画延迟来创建交错效果。
@for $i from 1 to 5 {
&:nth-child(#{$i + 1}) {
animation-delay: #{$i * 0.05}s;
}
}
上面的代码基本上是在查找每个子元素,然后为找到的每个元素添加 0.05 秒的延迟。例如,第一个子元素的延迟为 0.05 秒,第二个子元素的延迟为 0.10 秒,依此类推。
看看这一切看起来多么漂亮和自然。
额外内容:微交互!
使用动画的乐趣之一在于思考细节,因为它们是将事物联系在一起并为用户体验增添乐趣的关键。我们称这些为微交互,它们通过在执行操作时提供视觉反馈来发挥良好的作用。
根据动画的复杂程度,我们可能需要一个像anime.js或GSAP这样的库。这个例子非常简单,因此我们可以通过编写一些 CSS 来完成所有需要做的事情。
第一个微交互:音量图标
首先,让我们获取 SVG 格式的音量图标(Noun Project 和 Material Design 是不错的来源)。点击时,我们将对它的 path
元素进行动画淡入和淡出,以显示音量级别。为此,我们将创建一个方法,根据音量级别切换其 CSS 类。
<svg @click="changeVolume">
<g :class="`level-${volumeLevel}`">
<path d="..."/> <!-- volume level 1 -->
<path d="..."/> <!-- volume level 2 -->
<path d="..."/> <!-- volume level 3 -->
<polygon points="..."/>
</g>
</svg>
基于此类,我们可以显示和隐藏某些 path
元素,如下所示:
path {
opacity: 0;
transform-origin: left;
transform: translateX(-5px) scale(0.6);
transition: transform 0.25s, opacity 0.2s;
}
.level-1 path:first-child,
.level-2 path:first-child,
.level-2 path:nth-child(2),
.level-3 path {
opacity: 1;
transform: none;
}

第二个微交互:收藏图标
当你点击 Twitter 的心形按钮时,你喜欢这种感觉吗?这是因为它在点击时的动画方式显得独特且特别。我们将制作类似的东西,但速度很快。为此,我们首先获取一个 SVG 心形图标并将其添加到标记中。然后,我们将为其添加一个在点击时触发的弹跳动画。
@keyframes bounce {
0%, 100% {
transform: none;
}
30% {
transform: scale(1.3);
}
60% {
transform: scale(0.9);
}
}
我们还可以做的另一件有趣的事情是在它周围添加其他一些随机大小和位置的小心形图标。理想情况下,我们会添加一些具有心形背景的 absolute
定位的 HTML 元素。让我们通过设置它们的 left
和 bottom
值来如下排列它们。
我们还将包含一个淡出效果,以便图标在向上移动时看起来像溶解一样,方法是在相同的点击事件上添加关键帧动画。
@keyframes float-upwards {
0%, 100% {
opacity: 0;
}
50% {
opacity: 0.7;
}
50%, 100% {
transform: translate(-1px, -5px);
}
}

总结
就是这样!我希望你发现所有这些都有助于你在自己的网站和项目中尝试动画。
在撰写本文时,我还想扩展我们之前略过的基本动画原理,因为我相信它们有助于选择动画时长,并避免无意义的动画。讨论这一点很重要,因为**正确地**制作动画比根本不制作动画要好。但这听起来像是将来文章中要讨论的另一个完整主题。
精彩的深入文章!不过,感觉好像缺少了一个重要的主题:可访问性。
建议
通过 JavaScript 提供一个可见的切换按钮来关闭所有动画(https://egghead.io/lessons/aria-accessible-animations-with-reduced-motion – 如果你不是 egghead 会员,请查看文字记录)
检查用户是否在操作系统级别设置了
prefers-reduced-motion: reduce
。如果是,则禁用动画(https://mdn.org.cn/en-US/docs/Web/CSS/@media/prefers-reduced-motion)我决定在 React 中实现它。我遇到了一个问题,即返回时的动画无法缓慢返回,而是会立即返回。它会根据屏幕大小和图像容器顶部的填充而有所不同。有什么线索吗?
屏幕大小不应该有影响,因为点击事件会返回图像的绝对位置。
对于返回动画,请确保在较小的图像(艺术家网格中的图像)上应用 @keyframes 动画。这是因为当你点击后退按钮时,Tracks 页面会立即从 DOM 中移除。
你好!这个项目给了我极大的灵感。我学到了很多很棒的东西,我可以说花几天时间理解它背后的逻辑绝对是值得的。
不过,我有一些问题。
根据你的经验,充分利用像这样的项目最好的方法是什么?我注意到很多有趣的模式,我想确保能够在以后的项目中应用它们。
我觉得一个好方法是告诉其他人。不仅是告诉,而是根据我的理解解释事情是如何运作的。
这引出了我的下一个问题:如果我使用你的项目作为一篇文章的主题,并在其中解释大多数重要功能是如何实现的,你介意吗?
感谢你的时间!