六:CSS 核心机制——动画

一、动画:让界面“活”起来的最后一块拼图

前面几篇,我们依次拆解了 CSS 的选择器、属性值确定机制和布局系统。现在来到 CSS 核心机制的最后一环——动画

动画不是“锦上添花”的装饰品。在用户体验中,动画承担着关键功能:

  • 提供反馈:按钮被按下时微微下沉,让用户知道操作已生效。
  • 引导注意力:新消息弹出时带一个弹入效果,把用户的目光吸引过去。
  • 建立空间感:页面切换时的滑动效果,帮用户理解“我从哪来,到哪去”。
  • 掩盖加载时间:骨架屏的闪烁动画,让等待不那么焦虑。

CSS 提供了两套动画机制:过渡关键帧动画。前者适合简单的“从 A 到 B”的变化,后者适合复杂的、多阶段的动画序列。理解两者的适用场景和底层原理,是写出流畅动画的前提。

二、过渡:让变化变得平滑

过渡是最简单的动画形式。它告诉浏览器:“当这个属性发生变化时,不要瞬间切换,而是在一段时间内平滑过渡。”

基本语法

transition: 属性名 持续时间 缓动函数 延迟时间;

四个子属性:

属性 作用 示例
transition-property 要过渡的 CSS 属性 opacitytransformbackground
transition-duration 过渡持续时间 0.3s500ms
transition-timing-function 缓动函数,控制变化速率 ease(默认)、linearease-in-out
transition-delay 延迟多久后开始过渡 0s(默认)、0.2s

最简单的示例:hover 时按钮变色

button {
  background: #4a90d9;
  transition: background 0.3s ease;
}

button:hover {
  background: #357abd;
}

当鼠标移入按钮时,背景色从蓝色平滑过渡到深蓝色,耗时 0.3 秒。鼠标移出时,同样平滑过渡回蓝色。不需要写任何 JavaScript。

过渡多个属性

.card {
  background: white;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  transform: translateY(0);
  transition: background 0.3s, box-shadow 0.3s, transform 0.3s;
}

.card:hover {
  background: #f8f9fa;
  box-shadow: 0 8px 24px rgba(0,0,0,0.15);
  transform: translateY(-4px);
}

鼠标悬停时,卡片的背景、阴影、位置同时平滑变化,制造“浮起来”的立体效果。

过渡的触发条件

过渡发生在属性值改变时。常见的触发方式:

  • 伪类状态变化:hover:focus:active
  • JavaScript 修改样式element.style.opacity = '0' 会触发 opacity 的过渡。
  • 类名变化:JS 给元素添加或移除 class,导致样式变化。

过渡的局限:它只能做“从 A 到 B”的简单动画。如果需要多阶段(先变大再变小再弹回来),或者需要动画自动开始(不依赖用户操作),就需要关键帧动画。

三、关键帧动画:掌控每一帧

@keyframes 让你定义动画的多个阶段,每个阶段可以设置不同的样式。然后用 animation 属性把动画应用到元素上。

定义关键帧

@keyframes 动画名 {
  0%   { /* 动画开始时的样式 */ }
  50%  { /* 动画进行到一半时的样式 */ }
  100% { /* 动画结束时的样式 */ }
}

百分比表示动画的进度。你也可以用 fromto 代替 0%100%

应用动画

.element {
  animation: 动画名 持续时间 缓动函数 延迟时间 播放次数 方向 填充模式;
}

八个子属性(分两组记忆):

属性 作用 常用值
animation-name 关键帧名称 自定义名称
animation-duration 持续时间 0.5s2s
animation-timing-function 缓动函数 easelinearcubic-bezier()
animation-delay 延迟时间 0s1s(可以为负值,表示从中间开始)
animation-iteration-count 播放次数 1(默认)、infinite(无限循环)、3
animation-direction 播放方向 normalreversealternate(来回)
animation-fill-mode 动画结束后保持什么状态 none(默认)、forwards(保持最后一帧)、backwardsboth
animation-play-state 播放或暂停 running(默认)、paused

经典示例:淡入上浮效果

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.card {
  animation: fadeInUp 0.5s ease-out both;
}

元素从透明、下方 20px 的位置,淡入并上浮到原位。both 表示:动画开始前就应用第一帧的样式(防止闪烁),动画结束后保持最后一帧的样式。

多个动画同时作用

用逗号分隔多个动画:

.spinner {
  animation: spin 1s linear infinite, pulse 2s ease-in-out infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.5; }
}

四、缓动函数:动画的“性格”所在

同样是从 A 到 B、同样耗时 0.3 秒,不同的缓动函数会让动画呈现出完全不同的“性格”。

预设的缓动函数

  • linear:匀速。机械、生硬,自然界几乎不存在匀速运动。
  • ease(默认):慢→快→慢。柔和自然,适合大多数过渡效果。
  • ease-in:慢→快。适合元素退场(慢慢开始,加速消失)。
  • ease-out:快→慢。适合元素入场(快速出现,慢慢停稳)。
  • ease-in-out:慢→快→慢,比 ease 更对称。适合循环动画。

自定义缓动:cubic-bezier()

所有缓动函数本质都是贝塞尔曲线。cubic-bezier(x1, y1, x2, y2) 让你自定义曲线的形状:

/* 弹性效果 */
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.27, 1.55);

四个参数分别控制两个控制点的坐标。你可以在 Chrome 开发者工具中直观地拖拽曲线来调整参数,然后复制生成的 cubic-bezier() 值。

实战建议:不要对所有元素使用完全相同的缓动函数。入口动画用 ease-out(快进慢停),出口动画用 ease-in(慢出快走),状态切换用 easeease-in-out

五、性能:为什么 transformopacity 是动画首选

不是所有 CSS 属性都适合做动画。从渲染性能的角度,CSS 属性分为三个梯队。

第一梯队(只触发合成,性能最好)

transformopacity 是仅有的两个不会触发重排或重绘的属性。它们的动画在 GPU 上完成,不占用主线程。

  • transform: translateX()translateY()scale()rotate():移动、缩放、旋转。
  • opacity:透明度变化。

原则:能用 transform 实现的移动,绝不用 left/topmargin。前者性能好得多。

第二梯队(触发重绘,不触发重排)

colorbackground-colorbox-shadowborder-color 等只影响元素外观、不影响布局的属性。它们的动画需要 CPU 重新绘制,但不会重新计算布局。

第三梯队(触发重排,性能最差,应避免)

widthheightmarginpaddinglefttopfont-size 等影响元素尺寸或位置的属性。修改它们会导致浏览器重新计算布局(reflow),然后重绘(repaint),开销最大。

如果需要改变元素尺寸,考虑用 transform: scale() 代替 width/height 的变化。如果需要移动元素,用 transform: translate() 代替 left/topmargin

性能检查清单

  1. 动画中使用最多的属性应该是 transformopacity
  2. 避免在动画中修改 widthheightlefttopmarginpadding
  3. 如果必须在动画中使用第二梯队属性(如 background-color),确保动画元素被提升到独立的合成层:使用 will-change: transformtransform: translateZ(0)(但不要滥用)。
  4. 用 Chrome 开发者工具的 Performance 面板录制动画,检查帧率是否稳定在 60fps。

六、will-change:提前告诉浏览器“我要动”

will-change 属性让你提前通知浏览器某个元素即将发生变化,浏览器可以提前做优化准备(比如将该元素提升到独立的合成层)。

.card {
  will-change: transform;
}

使用原则:

  • 不要给太多元素加:每个合成层都会消耗 GPU 内存。只给确实会频繁动画的元素加。
  • 在动画开始前添加,动画结束后移除:JS 可以在 mouseenter 时加 will-change,在 animationend 时移除。
  • 不要“以防万一”地加:浏览器有自己的优化策略,滥用 will-change 反而可能降低性能。

七、animation-delay 为负值:从中间开始播放

这是一个不太为人知但非常实用的技巧。将 animation-delay 设为负值,可以让动画从一个中间状态开始播放:

/* 动画总时长 2s,delay 为 -1s → 跳过前半段,从 50% 处开始 */
.spinner:nth-child(1) { animation-delay: 0s; }
.spinner:nth-child(2) { animation-delay: -1.5s; }
.spinner:nth-child(3) { animation-delay: -0.3s; }

利用这个技巧,可以让多个相同动画的元素呈现出错落有致的节奏感,而不需要为每个元素定义不同的关键帧。

八、综合演示:一个完整的小动画系统

下面这段代码综合运用了过渡、关键帧动画、缓动函数和性能最佳实践:

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>CSS 动画综合演示</title>
  <style>
    body {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      margin: 0;
      background: #f0f4f8;
      font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
    }

    .demo-container {
      display: flex;
      flex-direction: column;
      gap: 32px;
      width: 400px;
    }

    /* ==================== 演示一:过渡 ==================== */
    .btn {
      padding: 14px 32px;
      background: #4a90d9;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 16px;
      cursor: pointer;
      transition: background 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease;
    }

    .btn:hover {
      background: #357abd;
      transform: translateY(-2px);
      box-shadow: 0 6px 20px rgba(74, 144, 217, 0.4);
    }

    .btn:active {
      transform: translateY(0);
      box-shadow: 0 2px 8px rgba(74, 144, 217, 0.2);
    }

    /* ==================== 演示二:关键帧动画 - 加载动画 ==================== */
    .loader-row {
      display: flex;
      justify-content: center;
      gap: 8px;
    }

    .dot {
      width: 12px;
      height: 12px;
      background: #4a90d9;
      border-radius: 50%;
      animation: bounce 1.4s ease-in-out infinite both;
    }

    .dot:nth-child(1) { animation-delay: 0s; }
    .dot:nth-child(2) { animation-delay: -0.2s; }
    .dot:nth-child(3) { animation-delay: -0.4s; }

    @keyframes bounce {
      0%, 80%, 100% {
        transform: scale(0.6);
        opacity: 0.4;
      }
      40% {
        transform: scale(1);
        opacity: 1;
      }
    }

    /* ==================== 演示三:卡片入场动画 ==================== */
    .card {
      background: white;
      padding: 24px;
      border-radius: 12px;
      box-shadow: 0 4px 16px rgba(0,0,0,0.08);
      animation: cardIn 0.6s ease-out both;
    }

    .card:nth-child(1) { animation-delay: 0.1s; }
    .card:nth-child(2) { animation-delay: 0.2s; }
    .card:nth-child(3) { animation-delay: 0.3s; }

    @keyframes cardIn {
      from {
        opacity: 0;
        transform: translateY(24px);
      }
      to {
        opacity: 1;
        transform: translateY(0);
      }
    }

    /* ==================== 演示四:脉冲提示 ==================== */
    .badge {
      display: inline-block;
      width: 10px;
      height: 10px;
      background: #e74c3c;
      border-radius: 50%;
      animation: pulse 2s ease-in-out infinite;
    }

    @keyframes pulse {
      0%, 100% {
        opacity: 1;
        transform: scale(1);
      }
      50% {
        opacity: 0.5;
        transform: scale(1.3);
      }
    }

    /* ==================== 演示五:骨架屏 ==================== */
    .skeleton {
      height: 16px;
      background: linear-gradient(90deg, #e0e0e0 25%, #f0f0f0 50%, #e0e0e0 75%);
      background-size: 200% 100%;
      animation: shimmer 1.5s ease-in-out infinite;
      border-radius: 4px;
    }

    @keyframes shimmer {
      0% {
        background-position: 200% 0;
      }
      100% {
        background-position: -200% 0;
      }
    }

    .skeleton-card {
      display: flex;
      flex-direction: column;
      gap: 12px;
      background: white;
      padding: 24px;
      border-radius: 12px;
      box-shadow: 0 4px 16px rgba(0,0,0,0.08);
    }

    .skeleton-title {
      height: 20px;
      width: 60%;
    }

    .skeleton-text {
      height: 14px;
      width: 100%;
    }

    .skeleton-text.short {
      width: 80%;
    }
  </style>
</head>
<body>

  <div class="demo-container">

    <!-- 演示一:按钮过渡 -->
    <button class="btn">悬停看我</button>

    <!-- 演示二:加载动画 -->
    <div class="loader-row">
      <div class="dot"></div>
      <div class="dot"></div>
      <div class="dot"></div>
    </div>

    <!-- 演示三:卡片入场 -->
    <div class="card">
      <h3>通知一</h3>
      <p>你的文章已发布。</p>
    </div>

    <div class="card">
      <h3>通知二</h3>
      <p>你有一条新评论。</p>
    </div>

    <!-- 演示四:脉冲提示 -->
    <div style="display: flex; align-items: center; gap: 8px;">
      <span class="badge"></span>
      <span style="color: #666; font-size: 14px;">有新消息</span>
    </div>

    <!-- 演示五:骨架屏 -->
    <div class="skeleton-card">
      <div class="skeleton skeleton-title"></div>
      <div class="skeleton skeleton-text"></div>
      <div class="skeleton skeleton-text short"></div>
      <div class="skeleton skeleton-text"></div>
    </div>

  </div>

</body>
</html>

逐块解析:

  • 按钮过渡hover 时背景、阴影、位移三项同时平滑过渡,active 时按钮归位。这是微交互的标准写法。
  • 加载动画:三个圆点用相同的 @keyframes,但通过不同的负 animation-delay 制造错落感。这是负 delay 技巧的经典应用。
  • 卡片入场:每个卡片依次从下方淡入上浮,通过递增的 animation-delay 实现依次出现的效果。both 确保动画前后都保持正确的状态。
  • 脉冲提示:红色圆点周期性缩放和变透明,模拟“呼吸”效果,用于提示用户注意。
  • 骨架屏:用渐变色背景 + 位移动画模拟加载中的占位效果,比静态的灰色方块更有“正在加载”的感觉。这是目前主流的加载占位方案。

九、本篇小结

这一篇我们系统学习了 CSS 的动画机制:

  • 过渡:最简单的动画形式,在属性值发生变化时平滑过渡。transition: 属性 时长 缓动 延迟;。适合 hover 效果、状态切换。
  • 关键帧动画:用 @keyframes 定义多阶段动画,用 animation 属性应用。适合加载动画、入场动画、循环动画。
  • 缓动函数:决定动画的“性格”。ease-out 适合入场,ease-in 适合退场,ease-in-out 适合循环。可通过 cubic-bezier() 自定义。
  • 性能最佳实践:动画应优先使用 transformopacity(只触发合成),避免 width/height/left/top(触发重排)。will-change 可提前通知浏览器优化,但不应滥用。
  • 负 delay 技巧:让动画从中间状态开始,配合不同的 delay 值制造错落节奏。

至此,CSS 的四个核心机制——选择器、属性值确定、布局、动画——全部讲完。从下一篇开始,我们将进入 JavaScript 的深层世界。

下一篇预告

下一篇,我们将开启 JS 篇——《JavaScript 简史与面向对象之道》。你会了解 JavaScript 是如何在 10 天内被创造出来的,它的原型继承机制到底是怎么回事,以及为什么说“JavaScript 中的一切皆对象”这句话既对又不对。

前端,每周更新。

© 版权声明
THE END
喜欢就支持一下吧
点赞6 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容