一个让用户"哇"出来的动效
2015 年,我在一个旅游网站工作。产品经理把一个设计稿扔到我面前:首页的 banner 上,一行标题从左侧滑入,背景图片缓慢缩放,下面的卡片随着页面滚动一个一个淡入。他淡淡地说:"这种效果在别的网站上看起来很酷,我们也做吧。"
我心想:不难。用 CSS transition 做悬停效果,用 @keyframes 做入场动画,用 scroll 事件监听滚动位置,给卡片加 class...... 吭哧吭哧写了三天,JS 里堆满了 addEventListener、getBoundingClientRect、requestAnimationFrame,还要处理节流、销毁事件。上线后,用户在低端手机上滑动时明显掉帧,产品经理指着 Chrome 的 "jank" 警告,问我为什么不能"丝滑一点"。
那时候我不知道,有一种叫做 scroll-driven animations 的 CSS 特性即将出现,它可以把滚动动画完全交给浏览器合成器线程,不再占用主线程。更不知道,几年后 view-transitions 会让页面切换像原生 App 一样平滑。
如今是 2026 年,CSS 动画已经从一个"点缀"升级为交互的核心手段。这篇文章,我会带你完整梳理 CSS 动画的四大支柱:过渡 、关键帧动画 、滚动驱动动画 、视图过渡。既有基础概念,也有实战案例和未来展望。读完你会明白,如何用最少的代码,实现让用户"哇"出来的流畅体验。
第一章:过渡(transition)------ 动画的基石
1.1 从"突变"到"渐变"
transition 可能是 CSS 里最简单的动画工具。它让属性值的变化不是瞬间完成,而是在一段时间内平滑过渡。
css
.button {
background-color: blue;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: red;
}
当你鼠标悬停时,背景色从蓝色渐变为红色,耗时 0.3 秒。如果没有 transition,它会"啪"地一下变红,毫无过渡。
1.2 可过渡的属性
不是所有属性都能过渡。只有那些可插值的属性才可以,即数值、颜色、长度、百分比等。例如:
- 颜色:
color,background-color,border-color - 尺寸:
width,height,padding,margin,font-size - 变换:
transform - 透明度:
opacity - 阴影:
box-shadow,text-shadow
不能过渡的属性包括:display(可以用 visibility 过渡)、position(但 top/left 可以,只要它们有数值变化)、z-index 等。
1.3 transition 的完整语法
css
.el {
transition-property: all; /* 要过渡的属性,all 表示所有 */
transition-duration: 0.3s; /* 持续时间 */
transition-timing-function: ease; /* 缓动函数 */
transition-delay: 0s; /* 延迟时间 */
}
/* 简写 */
.el {
transition: all 0.3s ease 0s;
}
缓动函数决定动画的速度曲线:
ease:慢-快-慢,默认linear:匀速ease-in:加速ease-out:减速ease-in-out:先加速后减速cubic-bezier():自定义贝塞尔曲线
1.4 实战技巧:使用 transform 代替 top/left 做位移
transform 在合成器线程中执行,不会触发布局重排(reflow),性能远优于 top/left。
css
/* 不推荐 */
.box:hover { left: 100px; }
/* 推荐 */
.box:hover { transform: translateX(100px); }
1.5 多个属性分别过渡
css
.btn {
transition: transform 0.2s ease, background-color 0.3s linear, box-shadow 0.15s;
}
1.6 过渡结束事件
JavaScript 中可以监听 transitionend 事件,在动画完成后执行逻辑。
js
element.addEventListener('transitionend', () => {
console.log('过渡结束');
});
过渡简单、兼容性好,是现代 Web 动效的基石。但它只能实现"状态变化"的动画,无法做循环或复杂序列动画。这时就需要关键帧动画。
第二章:关键帧动画(@keyframes)------ 表达复杂时序
2.1 定义关键帧
@keyframes 让你定义动画的中间状态(关键帧),浏览器自动插值中间帧。
css
@keyframes slideIn {
0% {
transform: translateX(-100%);
opacity: 0;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
.box {
animation: slideIn 0.5s ease-out;
}
也可以使用 from 和 to 替代 0% 和 100%。
2.2 animation 属性详解
css
.box {
animation-name: slideIn;
animation-duration: 1s;
animation-timing-function: ease;
animation-delay: 0s;
animation-iteration-count: infinite; /* 循环次数 */
animation-direction: alternate; /* 方向:normal, reverse, alternate, alternate-reverse */
animation-fill-mode: forwards; /* 动画结束后保持最后一帧状态 */
animation-play-state: running; /* 播放或暂停 */
}
/* 简写 */
.box {
animation: slideIn 1s ease 0s infinite alternate forwards;
}
2.3 多个关键帧与百分比
你可以定义任意多的关键帧,实现复杂运动。
css
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-30px);
}
80% {
transform: translateY(-10px);
}
}
2.4 无限循环与方向
css
.loader {
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
2.5 动画与过渡的配合
通常用过渡做悬停、焦点等交互反馈,用关键帧做入场、循环、吸引注意的动效。
第三章:滚动驱动动画(Scroll-driven Animations)------ 新时代的杀手锏
这是 2023 年以来 CSS 最激动人心的新特性之一。过去,滚动视差、滚动进度条等效果必须用 JS 监听滚动事件,性能差、代码臃肿。现在可以用 CSS 直接声明:"当滚动到某个位置时,动画完成百分之多少"。
3.1 核心概念
滚动驱动动画分为两种:
- 滚动进度动画(Scroll Progress):动画进度与滚动容器的滚动位置绑定。
- 视图进度动画(View Progress):动画进度与元素在视口中的可见度绑定。
3.2 滚动进度动画
假设你有一个进度条,随着页面滚动而填充:
css
@keyframes grow {
from { width: 0%; }
to { width: 100%; }
}
.progress {
animation: grow linear;
animation-timeline: scroll(); /* 关键!绑定滚动进度 */
position: fixed;
top: 0;
left: 0;
height: 4px;
background: blue;
}
animation-timeline: scroll() 表示动画的进度由最近的滚动容器的滚动位置决定。scroll() 可以指定滚动轴:scroll(block)(默认,垂直滚动)或 scroll(inline)(水平滚动)。
你也可以指定滚动容器:scroll(scroller nearest)。
3.3 视图进度动画
当某个元素进入视口时,动画开始;完全离开时,动画结束。
css
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.card {
animation: fadeIn linear;
animation-timeline: view(); /* 基于元素自身的视图进度 */
animation-range: entry 0% entry 100%; /* 可选,定义动画范围 */
}
animation-range 进一步控制动画的起止点。例如:
entry 0% entry 100%:元素从开始进入视口到完全进入,动画从 0% 到 100%。exit 0% exit 100%:元素从开始离开到完全离开。contain 0% contain 100%:元素完全在视口内时触发。
3.4 实战:滚动视差
css
.parallax {
animation: parallax linear;
animation-timeline: scroll();
animation-range: exit 0% exit 200%;
transform-origin: center;
}
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(200px); }
}
当元素向上滚动离开视口时,它会以较慢的速度移动,产生视差效果。
3.5 浏览器支持
截至 2026 年,滚动驱动动画已在 Chrome 115+、Edge 115+、Firefox 110+(需启用标志)、Safari 16.4+ 中得到部分支持。完整支持仍在推进,但生产环境可通过 @supports 渐进增强。
css
@supports (animation-timeline: scroll()) {
.progress {
animation-timeline: scroll();
}
}
3.6 与 GSAP 等库的对比
GSAP 的 ScrollTrigger 插件功能极其强大,但滚动驱动动画的优势是原生、高性能(运行在合成器线程),适合简单到中度的滚动联动效果。复杂的时间线、交错动画仍然需要 JS 库。
第四章:视图过渡(View Transitions)------ SPA 级别的平滑切换
视图过渡是另一个 2023--2024 年间推出的革命性特性。它让同一个 DOM 在不同状态之间切换时(例如页面跳转、列表重排),可以产生流畅的过渡动画,无需手动编写复杂的 DOM 状态管理。
4.1 核心 API
视图过渡的核心是 document.startViewTransition() 方法。当你调用它并传入一个更新 DOM 的回调时,浏览器会:
- 捕获当前页面的快照(截图)。
- 执行回调,更新 DOM。
- 捕获新状态的快照。
- 在两个快照之间创建过渡动画(默认是淡入淡出)。
js
function spaNavigate(url) {
if (!document.startViewTransition) {
// 降级:直接跳转
window.location.href = url;
return;
}
document.startViewTransition(() => {
// 更新 DOM 到新页面状态
updateDOMForPage(url);
});
}
4.2 CSS 定制过渡效果
你可以通过伪元素 ::view-transition-old 和 ::view-transition-new 自定义动画:
css
/* 默认效果:淡入淡出 */
::view-transition-old(root) {
animation: fade-out 0.3s ease;
}
::view-transition-new(root) {
animation: fade-in 0.3s ease;
}
更高级的用法:为不同元素指定独立的过渡组。使用 view-transition-name 属性。
css
.header {
view-transition-name: header;
}
.sidebar {
view-transition-name: sidebar;
}
然后就可以针对 header 和 sidebar 分别定义过渡动画,比如让标题从左边飞入,侧边栏从右边飞入。
4.3 多页面应用(MPA)中的视图过渡
2026 年,Chrome 和 Firefox 已经支持跨文档视图过渡 (Cross-document View Transitions),即在传统刷新页面式的导航中也能使用视图过渡。只需在 <link> 或 meta 中声明:
html
<link rel="stylesheet" href="style.css" media="navigation" />
或通过 JS 启用:
js
window.addEventListener('pagereveal', (e) => {
e.viewTransition.updateCallbackDone = () => {
// 新页面已加载但未渲染
};
});
4.4 实战:图片画廊的平滑切换
js
document.querySelectorAll('.thumbnail').forEach(thumb => {
thumb.addEventListener('click', async (e) => {
const fullImage = document.getElementById('full-image');
const imageUrl = thumb.dataset.large;
if (document.startViewTransition) {
document.startViewTransition(() => {
fullImage.src = imageUrl;
});
} else {
fullImage.src = imageUrl;
}
});
});
配合 CSS 过渡,可以实现大图从缩略图位置展开的效果。
4.5 注意事项
- 视图过渡会捕获页面截图,对性能有一定影响,不要滥用。
- 某些元素(如
fixed定位的元素)在过渡中可能表现异常,需要调整view-transition-name或避免。 - 早期版本有 bug,2026 年已基本稳定。
第五章:综合实战 ------ 一个完整的交互页面
我们将使用以上所有技术,构建一个产品展示页面:
- 头部导航条,滚动时背景模糊、阴影加深(过渡)。
- 页面加载时,标题从上方淡入(关键帧)。
- 滚动时,每个产品卡片依次淡入(视图进度动画)。
- 点击卡片,展开详情视图(视图过渡 + 关键帧)。
- 页面顶部显示阅读进度条(滚动进度动画)。
由于代码量较大,这里给出核心结构。
html
<header class="sticky-header">...</header>
<div class="progress-bar"></div>
<main>
<h1 class="page-title">我们的产品</h1>
<div class="card">...</div>
...
</main>
css
/* 头部滚动效果 */
.sticky-header {
transition: background-color 0.2s, box-shadow 0.2s;
}
.sticky-header.scrolled {
background: rgba(255,255,255,0.95);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
/* 进度条滚动动画 */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: #f90;
animation: grow linear;
animation-timeline: scroll();
}
@keyframes grow {
from { width: 0%; }
to { width: 100%; }
}
/* 卡片视图进度动画 */
.card {
animation: fadeUp linear;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(30px); }
to { opacity: 1; transform: translateY(0); }
}
/* 页面标题入场关键帧 */
.page-title {
animation: slideDown 0.5s ease-out;
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
js
// 视图过渡:点击卡片展开详情
document.querySelectorAll('.card').forEach(card => {
card.addEventListener('click', () => {
const details = card.querySelector('.details');
if (document.startViewTransition) {
document.startViewTransition(() => {
details.classList.toggle('expanded');
});
} else {
details.classList.toggle('expanded');
}
});
});
这个页面没有使用任何第三方库,却实现了现代 Web 动效的最高水平。
第六章:性能优化与最佳实践
6.1 坚持使用 transform 和 opacity
这两个属性只需合成器线程参与,不会触发重排或重绘。例如,移动元素用 translate,淡入淡出用 opacity,缩放用 scale。
6.2 使用 will-change 提前告知浏览器
如果某个元素即将发生动画,可以加上 will-change: transform; 提示浏览器创建独立图层,但不要滥用,否则内存过高。
6.3 减少动画层级
动画元素应尽量少,每一层动画都会增加 GPU 合成负担。
6.4 利用 requestAnimationFrame 进行 JS 动画
如果必须用 JS 控制动画,使用 requestAnimationFrame 而非 setTimeout,保证与屏幕刷新率同步。
6.5 优先使用 CSS 动画而非 JS
只要能用 CSS 实现,绝不写 JS。维护成本低,性能好。
6.6 避免动画导致页面跳字(jank)
使用 Chrome DevTools 的 Performance 面板,查看帧率、耗时长的任务。如果发现 Layout Shift,尝试改为 transform。
第七章:未来展望
CSS 动画的未来不止于此。我们还将看到:
- 更精细的滚动驱动控制:比如基于元素在视口中的位置比例,而非简单的进入/离开。
- 动画合成(Animation Composition):多个动画可以同时控制同一个属性,类似 GSAP 的时间线。
- 自定义贝塞尔曲线编辑:更直观的缓动函数设计。
- 视图过渡的进一步普及:跨文档过渡成为默认,甚至支持返回手势动画。
让交互成为体验的灵魂
从简单的悬停过渡,到复杂的关键帧序列,再到滚动驱动动画和视图过渡,CSS 已经从一个"样式语言"进化为一个完整的交互设计平台。如今,我们不再需要为了一个滚动视差而引入几十 KB 的 JS 库,不再需要为了页面切换的流畅而手写状态管理。
你可以用两行 CSS 实现滚动进度条,用 view() 让卡片随着滚动渐入,用 startViewTransition 实现原生 App 般的页面切换。
动效不只是"花哨",它是用户理解界面的向导,是品牌个性的表达,是响应性的确认。在你的下一个项目中,试着多给按钮加一个微妙的过渡,给加载加一个旋转的动画,给卡片加一个优雅的入场动效。你会发现,用户会下意识地觉得"这个网站真流畅"。
CSS 动画的世界,值得我们不断探索。希望这篇文章能成为你进入这个世界的完整地图。