CSS过渡的交互式指南

212 阅读18分钟

简介

网络动画的世界已经成为一个由工具和技术组成的无序的丛林。像GSAP、Framer Motion和React Spring这样的库已经涌现出来,帮助我们在DOM中添加动作。

然而,最基本和最关键的部分是简陋的CSS过渡。它是大多数前端开发人员学习的第一个动画工具,而且是一个主力。即使是最灰头土脸、饱经风霜的动画老手也会经常使用这个工具。

这个话题的深度令人惊讶。在本教程中,我们将深入了解CSS转场,以及如何使用它们来创建生动、精美的动画。

目标受众

本教程的目的是让所有经验水平的开发者都能理解。它可以被认为是 "CSS转换101"。尽管如此,我还是在其中加入了一些有趣和晦涩的小知识--无论你的经验水平如何,我打赌你都能学到一些东西

基本原理

我们创建一个动画所需要的主要成分是一些会变化的CSS。

下面是一个按钮的例子,它在悬停时移动,但没有动画

Code Playground

<button class="btn">
  Hello World
</button>

<style>
  .btn {
    width: 100px;
    height: 100px;
    border-radius: 50%;
    border: none;
    background: slateblue;
    color: white;
    font-size: 20px;
    font-weight: 500;
    line-height: 1;
  }
  
  .btn:hover {
    transform: translateY(-10px);
  }
</style>

这个片段使用:hover 伪类来指定一个额外的CSS声明,当用户的鼠标停留在我们的按钮上面时,类似于JavaScript中的onMouseEnter 事件。

为了使元素上移,我们使用transform: translateY(-10px) 。虽然我们可以使用margin-top ,但transform: translate 是一个更好的工具。我们将在后面看到原因。

在默认情况下,CSS中的变化是瞬间发生的。眨眼间,我们的按钮就被传送到了一个新的位置!这与CSS中的默认变化不一致。这与自然界的情况是不一致的,因为自然界的事情是逐渐发生的。

我们可以通过命名恰当的transition 属性来指示浏览器从一个状态插值到另一个状态。

代码游戏场

使用Prettier格式化代码

<button class="btn">
  Hello World
</button>

<style>
  .btn {
    /*
      All of the base styles have
      moved to the “CSS” tab above.
    */
    transition: transform 250ms;
  }
  
  .btn:hover {
    transform: translateY(-10px);
  }
</style>

transition 可以取多个值,但只有两个是必需的。

  1. 我们希望动画化的属性名称

  2. 动画的持续时间

如果你打算给多个属性做动画,你可以给它传递一个逗号分隔的列表。

.btn {
  transition: transform 250ms, opacity 400ms;
}
.btn:hover {
  transform: scale(1.2);
  opacity: 0;
}

选择所有属性

transition-property 需要一个特殊的值: 。当指定 ,任何改变的CSS属性都会被过渡。all all

使用这个值是很诱人的,因为如果我们要对多个属性进行动画处理,它可以为我们节省一大块输入的时间,但我建议不要使用它。

随着你的产品的发展,你(或你的团队中的某人)很可能会在未来的某个时间点上更新这段代码。一个意外的动画可能会溜走。

动画就像盐:太多的话会破坏这道菜。我们需要对动画的属性进行精确的处理。

定时功能

当我们告诉一个元素从一个位置过渡到另一个位置时,浏览器需要计算出每个 "中间 "帧应该是什么样子。

例如:假设我们将一个元素从左到右移动,持续时间为1秒。一个流畅的动画应该以60fps*的速度运行,这意味着我们需要在开始和结束之间想出60个独立的位置。

为了澄清这里发生的事情:每个褪色的圆圈代表一个时间点。当圆圈从左到右移动时,这些就是显示给用户的帧。这就像一本翻页书。

在这个动画中,我们使用的是一个线性计时函数。这意味着元素以恒定的速度移动;我们的圆圈每一帧的移动量是一样的。

image.png

在CSS中,有几个计时函数可供我们使用。我们可以通过transition-timing-function 属性指定我们要使用的函数。

.btn {
  transition: transform 250ms;
  transition-timing-function: linear;
}

或者,我们可以直接将其传递给transition 速记属性。

.btn {
  transition: transform 250ms linear;
}

linear 很少是最好的选择--毕竟,现实世界中几乎没有什么东西是这样移动的*。好的动画都是模仿自然界的,所以我们应该选择一些更有机的东西!

让我们运行一下我们的选择。

易出

ease-out 像一头野牛一样冲过来,但它的能量耗尽了。到最后,它就像一只困倦的乌龟一样蹒跚而行。

试着用时间线刷一下;注意到前几帧的运动是多么剧烈,而到了最后又变得多么微妙。

如果我们把元素的位移随时间变化的图形化,它看起来会是这样的。 image.png

你什么时候会使用ease-out ?当有东西从屏幕外进入时,它是最常用的(例如,一个模态出现)。它产生的效果是,有东西从远处匆匆而来,并在用户面前落定。

易进

ease-in不出所料,它与ease-out 相反。它开始时很慢,然后加速。

正如我们所看到的,ease-out 对于从屏幕外进入视野的事物是有用的。ease-in自然,它对相反的情况也很有用:把东西移到视口的边界之外。

image.png

这个组合在东西进入和离开视口时很有用,比如一个模态。我们很快就会看到如何混合和匹配计时函数。

请注意,ease-in 几乎只适用于在元素离开屏幕或不可见的情况下结束的动画;否则,突然停止可能会很刺耳。

易-进-出

接下来是ease-in-out 。它是前面两个计时函数的组合。

这个计时函数是对称的。它有等量的加速和减速。 image.png

我发现这条曲线对任何发生在循环中的东西最有用(例如,一个元素淡入淡出,一遍又一遍)。

它比linear ,但在你把它用在所有东西上之前,让我们再看看一个选项。

放松

如果说我对CSS语言的作者在过渡方面有什么不满的话,那就是ease ,这个名字太差了。它完全没有描述性;从字面上看,所有的定时功能都是这样或那样的缓和。

撇开这个挑剔不谈,ease 是非常棒的。不像ease-in-out ,它不是对称的;它有一个短暂的上升和大量的减速。

ease 是默认值--如果你不指定一个计时函数,就会使用 。老实说,这对我来说是正确的。在大多数情况下, 是一个不错的选择。如果一个元素移动,并且没有进入或退出视口, 通常是一个好的选择。ease ease ease

image.png

时间是恒定的

关于所有这些演示的一个重要说明:时间是恒定的。定时函数描述的是一个值在一个固定的时间间隔内应该如何从0到1,而不是动画应该如何快速完成。有些计时函数可能感觉比较快或比较慢,但在这些例子中,它们都正好需要1秒来完成。

自定义曲线

如果提供的内置选项不适合你的需要,你可以定义自己的自定义缓和曲线,使用立方贝塞尔计时函数

.btn {
  transition:
    transform 250ms cubic-bezier(0.1, 0.2, 0.3, 0.4);
}

到目前为止,我们所看到的所有数值实际上都只是这个cubic-bezier 函数的预设值。它需要4个数字,代表2个控制点。

贝塞尔曲线真的很有趣,但它们超出了本教程的范围。不过,我很快就会写更多关于它们的内容。

同时,你可以使用Lea Verou提供的这个奇妙的助手开始创建你自己的贝塞尔计时函数。

一旦你想出了一条你满意的动画曲线,点击顶部的 "复制",将其粘贴到你的CSS中

你也可以从这个扩展的定时函数集中挑选。不过要注意的是:一些比较离奇的选项在CSS中是不能用的。

image.png

在开始使用自定义贝塞尔曲线时,可能很难想出一条感觉自然的曲线。然而,经过一段时间的练习,这是一个极具表现力的工具。

是时候让我坦白了

我得承认:上面的演示显示了不同的计时功能,是夸大其词的。

事实上,像ease-in 这样的计时功能比描述的更微妙,但我想强调这种效果,使其更容易理解。cubic-bezier 定时功能使这成为可能!

动画性能

早些时候,我们提到,动画应该以60fps的速度运行。但是,当我们做数学计算时,我们意识到这意味着浏览器只有16.6毫秒的时间来绘制每一帧。这其实并没有多少时间;作为参考,我们眨眼的时间大约是100ms-300ms。

如果我们的动画计算量太大,它就会显得生硬和呆滞。帧数会减少,因为设备无法跟上。

通过调整新的 "每秒帧数 "控制,你可以亲自体验到这一点。

在实践中,糟糕的性能往往采取可变帧率的形式,所以这并不是一个完美的模拟。

动画性能是一个令人惊讶的、有趣的领域,远远超出了这个介绍性教程的范围。但让我们来介绍一下绝对关键的、需要了解的部分。

  1. 有些CSS属性的动画效果比其他属性要好得多。例如,height 是一个非常昂贵的属性,因为它影响到布局。当一个元素的高度缩小时,会引起连锁反应;它的所有同级元素也需要向上移动,以填补空间。

  2. 其他的属性,比如background-color ,对动画来说有些昂贵。它们不影响布局,但它们确实需要在每一帧上涂上一层新的油漆,这并不便宜。

  3. 有两个属性--transformopacity --做动画非常便宜。如果一个动画目前正在调整一个属性,如widthleft ,把它移到transform 就可以大大改善(尽管并不总是能够达到完全相同的效果)。

  4. 一定要在你的网站/应用程序所针对的最低端设备上测试你的动画。你的开发机器可能比它快很多倍。

如果你有兴趣了解更多关于动画性能的信息,我在React拉力赛上做了一个关于这个主题的演讲。它深入探讨了这个话题。

硬件加速

根据你的浏览器和操作系统,你可能已经注意到前面的一些例子中有一个奇怪的小瑕疵。

image.png

仔细注意这些字母。注意到它们在过渡的开始和结束时如何出现轻微的颤动,好像一切都在锁定到位?

这是因为计算机的CPU和GPU之间的交接而发生的。让我解释一下。

当我们使用transformopacity 来制作一个元素的动画时,浏览器有时会试图优化这个动画。它不对每一帧的像素进行光栅化处理,而是将所有内容作为纹理传输给GPU。GPU非常善于做这种基于纹理的转换,因此,我们可以得到一个非常流畅、性能非常好的动画。这就是所谓的 "硬件加速"。

问题是:GPU和CPU的渲染方式略有不同。当CPU把它交给GPU时,反之亦然,你会得到一个东西稍微移动的快感。

我们可以通过添加以下CSS属性来解决这个问题。

.btn {
  will-change: transform;
}

will-change 是一个属性,它允许我们向浏览器提示,我们将对所选元素进行动画处理,并且它应该为这种情况进行优化。

在实践中,这意味着浏览器将让GPU一直处理这个元素。不再有CPU和GPU之间的交接,不再有明显的 "捕捉到位"。

will-change 让我们对哪些元素应该被硬件加速是有意识的。浏览器在这个问题上有自己不可捉摸的逻辑,我宁愿不把它留给机会。

硬件加速还有一个好处:我们可以利用亚像素渲染的优势。

请看这两个盒子。当你悬停/聚焦它们时,它们会向下移动。其中一个是硬件加速的,而另一个则不是。

代码乐园

使用Prettier格式化代码

<style>
  .accelerated.box {
    transition: transform 750ms;
    will-change: transform;
    background: slateblue;
  }
  .accelerated.box:hover,
  .accelerated.box:focus {
    transform: translateY(10px);
  }
  
  .janky.box {
    transition: margin-top 750ms;
    will-change: margin-top;
    background: deeppink;
  }
  .janky.box:hover,
  .janky.box:focus {
    margin-top: 10px;
  }
</style>

<div class="wrapper">
  <button class="accelerated box"></button>
  <button class="janky box"></button>
</div>

这也许有点微妙,取决于你的设备和你的显示器,但一个盒子比另一个移动得更流畅。

margin-top 这样的属性不能进行次像素渲染,这意味着它们需要四舍五入到最接近的像素,从而产生一种阶梯式的、粗糙的效果。transform与此同时,由于GPU的抗锯齿技巧,可以在像素之间平滑地移动。

权衡利弊

生活中没有什么是免费的,硬件加速也不例外。

将一个元素的渲染委托给GPU,它将消耗更多的视频内存,这种资源可能是有限的,特别是在低端移动设备上。

这并不像以前那样是个大问题--我在小米红米7A上做了一些测试,这是一款在印度很受欢迎的经济型智能手机,它似乎可以维持得很好。只是不要在不会移动的元素上广泛使用will-change 。对你使用它的地方要有意为之。

替代属性

硬件加速已经存在了很长时间,事实上,比will-change 属性还要长。

在很长一段时间里,它是通过使用3D变换来完成的,比如transform: translateZ(0px) 。即使数值为0px,浏览器仍然会将其交给GPU,因为在3D空间中移动绝对是GPU的强项。还有backface-visibility: hidden

will-change 出现时,它的目的是为开发者提供一种适当的、有语义的方式,向浏览器提示某个元素应该被优化。

令人高兴的是,似乎所有这些问题都已得到解决。我做了一些测试,发现在现代浏览器中使用will-change ,可以得到最好的结果。但你应该总是做你自己的测试,以确保这些技术在你的目标设备和浏览器上发挥作用。

用户体验的触动

动作驱动的运动

让我们再看看我们正在上升的 "Hello World "按钮。

你好,世界

目前,我们有一个 "对称 "的过渡--进入动画和退出动画是一样的。

  • 当鼠标悬停在元素上时,它在250ms内向上移动了10个像素

  • 当鼠标移开时,元素在250ms内向下移动10个像素。

一个可爱的小细节是给每个动作都有自己的过渡设置。对于悬停动画,我喜欢让进入动画快速而敏捷,而退出动画则可以更轻松和慵懒一些。

代码操场

使用Prettier格式化代码

重置代码

HTML

<button class="btn">
  Hello World
</button>

<style>
  .btn {
    will-change: transform;
    transition: transform 450ms;
  }
  
  .btn:hover {
    transition: transform 125ms;
    transform: translateY(-10px);
  }
</style>

CSS

.btn {
  width: 100px;
  height: 100px;
  border-radius: 50%;
  border: none;
  background: slateblue;
  color: white;
  font-size: 20px;
  font-weight: 500;
  line-height: 1;
}

另一个常见的例子是模态。对于模态来说,进入时有一个ease-out 的动画,退出时有一个更快的ease-in 的动画,这可能很有用。

这是一个小细节,但它涉及到一个更大的想法。

我相信大多数开发者都是按状态来思考的:例如,你可能会看到这种情况,说我们有一个 "悬停 "状态和一个默认状态。相反,如果我们从动作的角度来思考呢?我们根据用户正在做的事情制作动画,从事件而不是状态的角度来思考。我们有一个鼠标进入的动画和一个鼠标离开的动画。

延迟

好了,在我们熟练掌握CSS过渡的过程中,我们已经走得很远了,但还有一些最后的细节需要去研究。让我们来谈谈过渡延迟的问题。

我相信,几乎每个人都有过这种令人沮丧的经历。

image.png

作为一个开发者,你可能会知道为什么会发生这种情况:下拉菜单只在被悬停的时候保持打开状态当我们斜向移动鼠标来选择一个子项时,我们的光标就会跳出范围,菜单就会关闭。

这个问题可以用一种相当优雅的方式来解决,而不需要去找JS。我们可以使用transition-delay!

.dropdown {
  opacity: 0;
  transition: opacity 400ms;
  transition-delay: 300ms;
}
.dropdown-wrapper:hover .dropdown {
  opacity: 1;
  transition: opacity 100ms;
  transition-delay: 0ms;
}

transition-delay 允许我们在一个短暂的时间内保持现状。在这种情况下,当用户把他们的鼠标移到 ,300ms内不会发生任何事情。如果他们的鼠标在这300ms的窗口内重新进入该元素,过渡就不会发生了。.dropdown-wrapper

300ms过后,transition 正常启动,下拉菜单在400ms内逐渐消失。

为什么没有速记?

到目前为止,我们一直在使用transition 速记,将所有与过渡有关的值捆绑在一起。transition-delay 也可以与速记一起使用。

.dropdown {
  opacity: 0;
  transition: opacity 250ms 300ms;
}

厄运闪现

当一个元素在悬停时被向上或向下移动时,我们需要非常小心,不要意外地引入一个 "末日闪烁"。

警告。这个GIF包含了闪烁的动作,对于有光敏性癫痫的人来说,有可能引发癫痫发作。

你可能已经注意到本页面上的一些演示有类似的效果

当鼠标靠近元素的边界时,麻烦就出现了。悬停效果将元素从鼠标下移开,这导致它重新落在鼠标下,这导致悬停效果再次触发......一秒钟内多次。

我们如何解决这个问题呢?诀窍是把触发效果分开。这里有一个快速的例子。

<style>
  @keyframes fade-out {
    from {
      opacity: 1;
    }
    to {
      opacity: 0;
    }
  }
  
  .box {
    animation: fade-out 1000ms;
  }
</style>

<div class="box">
  Hello World
</div>

代码操场

使用Prettier格式化代码

HTMLCSS

<button class="btn">
  <span class="background">
    Hello World
  </span>
</button>

<style>
  .background {
    will-change: transform;
    transition: transform 450ms;
  }
  
  .btn:hover .background {
    transition: transform 150ms;
    transform: translateY(-10px);
  }
  
  /* Toggle me on for a clue! */
  .btn {
    /* outline: auto; */
  }
</style>

我们的<button> ,现在有一个新的孩子,.background 。这个跨度容纳了所有的外观样式(背景颜色,字体等)。

当我们把鼠标放在普通的按钮上时,会使子句在上面探出头来。然而,这个按钮是静止的。

试着取消注释outline ,看看到底发生了什么事

尊重运动偏好

当我在网上看到一个精心制作的动画时,我的反应是高兴和欢喜。不过人们是不同的,有些人的反应非常不同:恶心和萎靡不振。

让我们在这里应用这些经验,为那些要求禁用动画的人禁用动画。

@media (prefers-reduced-motion: reduce) {
  .btn {
    transition: none;
  }
}

这个小调整意味着,对于进入系统偏好设置并切换了一个复选框的用户来说,动画将立即解决。

作为前端开发者,我们有一定的责任来确保我们的产品不会造成伤害。这是我们可以执行的一个快速步骤,使我们的网站/应用程序更友好、更安全。

大局观

CSS转换是最基本的,但这并不意味着它们很容易。它们的深度令人惊讶;即使在这篇冗长的博文中,我也不得不删掉一些东西以保持其可控性

网页动画比大多数开发者意识到的更重要。这里或那里的一个过渡不会影响或破坏一个体验,但它会增加。总的来说,执行良好的动画可以对整个用户体验产生令人惊讶的深远影响。

转场可以使一个应用程序感到 "真实"。它们可以提供反馈,并以一种比单独的文案更直观的方式进行交流。它们可以教人们如何使用你的产品。它们可以激发快乐。

它是建立在与本博客相同的技术栈上的,所以它具有相同的嵌入式交互式部件的风格,但它更进一步。

最后,没有 "沙盒模式 "的互动课程是不完整的你可以玩玩以前所有的设置(还有一些新的设置!),用这个开放式的小工具创造一些生成性艺术。