CSS 动画与过渡
大约 8 分钟约 2344 字
CSS 动画与过渡
简介
CSS 动画与过渡为页面元素添加运动效果,提升用户体验和感知性能。理解 transition、animation、@keyframes、transform 变换和性能优化,有助于创建流畅的界面。CSS 动画分为 transition(过渡,两个状态间平滑变化)和 animation(动画,通过 @keyframes 定义关键帧序列)。性能优化的核心原则是只动画 transform 和 opacity 属性,避免触发布局重排(Layout/Reflow)。
特点
实现
transition 过渡效果
/* ========== 基础过渡 ========== */
.btn {
background: #1890ff;
color: white;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
font-size: 14px;
/* all — 所有可动画属性
0.3s — 持续时间
cubic-bezier(0.4, 0, 0.2, 1) — 缓动函数 */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn:hover {
background: #40a9ff;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.4);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 2px 4px rgba(24, 144, 255, 0.3);
}
/* ========== 多属性不同时长 ========== */
.card {
transition: transform 0.3s ease, box-shadow 0.3s ease 0.1s, opacity 0.2s;
}
.card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
}
/* ========== 列表项交错动画 ========== */
.list-item {
opacity: 0;
transform: translateX(-20px);
transition: all 0.4s ease;
}
.list-item.active {
opacity: 1;
transform: translateX(0);
}
/* 交错延迟 — 使用 CSS 自定义属性 */
.list-item:nth-child(1) { transition-delay: 0.05s; }
.list-item:nth-child(2) { transition-delay: 0.1s; }
.list-item:nth-child(3) { transition-delay: 0.15s; }
.list-item:nth-child(4) { transition-delay: 0.2s; }
.list-item:nth-child(5) { transition-delay: 0.25s; }
/* ========== 常用缓动函数 ========== */
/* ease — 默认,开始和结束慢,中间快 */
/* ease-in — 开始慢,结束快 */
/* ease-out — 开始快,结束慢 */
/* ease-in-out — 开始和结束慢 */
/* linear — 匀速 */
/* cubic-bezier — 自定义 */
/* 弹性效果 */
.bounce-in {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.bounce-in:hover {
transform: scale(1.1);
}
/* ========== 展开收起动画 ========== */
.collapsible-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
padding: 0 16px;
}
.collapsible-content.open {
max-height: 500px; /* 需要设置一个足够大的值 */
padding: 16px;
}
/* ========== 颜色过渡 ========== */
.theme-toggle {
color: #333;
background: #fff;
transition: color 0.3s, background 0.3s;
}
[data-theme="dark"] .theme-toggle {
color: #e8e8e8;
background: #1f1f1f;
}@keyframes 关键帧动画
/* ========== 加载旋转 ========== */
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid #e8e8e8;
border-top-color: #1890ff;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* ========== 淡入上滑 ========== */
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in-up {
animation: fadeInUp 0.5s ease forwards;
}
/* ========== 脉冲效果 ========== */
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
.notification-badge {
animation: pulse 2s ease-in-out infinite;
}
/* ========== 打字机效果 ========== */
@keyframes typing {
from { width: 0; }
to { width: 100%; }
}
@keyframes blink {
50% { border-color: transparent; }
}
.typewriter {
overflow: hidden;
white-space: nowrap;
border-right: 2px solid #333;
width: 0;
animation: typing 3s steps(30) forwards, blink 0.7s step-end infinite;
}
/* ========== 呼吸灯 ========== */
@keyframes breathe {
0%, 100% { box-shadow: 0 0 4px rgba(24, 144, 255, 0.2); }
50% { box-shadow: 0 0 20px rgba(24, 144, 255, 0.6); }
}
.breathe {
animation: breathe 3s ease-in-out infinite;
}
/* ========== 骨架屏加载 ========== */
@keyframes skeleton-loading {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease infinite;
border-radius: 4px;
}
.skeleton-text {
height: 16px;
margin-bottom: 12px;
}
.skeleton-title {
height: 24px;
width: 60%;
margin-bottom: 16px;
}
/* ========== 交错入场动画 ========== */
@keyframes slideInRight {
from { opacity: 0; transform: translateX(30px); }
to { opacity: 1; transform: translateX(0); }
}
.stagger-item {
opacity: 0;
animation: slideInRight 0.4s ease forwards;
}
.stagger-item:nth-child(1) { animation-delay: 0.05s; }
.stagger-item:nth-child(2) { animation-delay: 0.1s; }
.stagger-item:nth-child(3) { animation-delay: 0.15s; }
.stagger-item:nth-child(4) { animation-delay: 0.2s; }
.stagger-item:nth-child(5) { animation-delay: 0.25s; }
/* ========== 弹窗缩放 ========== */
@keyframes modalIn {
from { opacity: 0; transform: scale(0.9); }
to { opacity: 1; transform: scale(1); }
}
@keyframes modalOut {
from { opacity: 1; transform: scale(1); }
to { opacity: 0; transform: scale(0.9); }
}
.modal-enter {
animation: modalIn 0.25s ease;
}
.modal-leave {
animation: modalOut 0.2s ease;
}动画控制与 JavaScript 集成
/* ========== animation 属性详细 ========== */
.animated-element {
/* name — @keyframes 名称 */
animation-name: fadeInUp;
/* duration — 持续时间 */
animation-duration: 0.5s;
/* timing-function — 缓动函数 */
animation-timing-function: ease;
/* delay — 延迟 */
animation-delay: 0.1s;
/* iteration-count — 播放次数(infinite 无限) */
animation-iteration-count: 1;
/* direction — 方向(normal/reverse/alternate) */
animation-direction: normal;
/* fill-mode — 填充模式(forwards 保持最后一帧) */
animation-fill-mode: forwards;
/* play-state — 播放状态(paused/running) */
animation-play-state: running;
/* 简写 */
animation: fadeInUp 0.5s ease 0.1s 1 normal forwards;
}
/* ========== 暂停/恢复动画 ========== */
.paused { animation-play-state: paused; }
.running { animation-play-state: running; }
/* 鼠标悬停暂停 */
.animated-element:hover {
animation-play-state: paused;
}// ========== JavaScript 控制动画 ==========
const element = document.querySelector('.animated-element');
// 暂停/恢复
element.style.animationPlayState = 'paused';
element.style.animationPlayState = 'running';
// 获取所有动画
const animations = element.getAnimations();
animations.forEach(a => a.pause());
animations.forEach(a => a.play());
// 监听动画事件
element.addEventListener('animationstart', (e) => {
console.log('动画开始:', e.animationName);
});
element.addEventListener('animationend', (e) => {
console.log('动画结束:', e.animationName);
});
element.addEventListener('animationiteration', (e) => {
console.log('动画循环:', e.animationName);
});
// ========== Web Animations API ==========
// 使用 JavaScript 精细控制动画
const keyframes = [
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0.5 },
{ transform: 'translateX(200px)', opacity: 1 },
];
const options = {
duration: 1000,
iterations: Infinity,
direction: 'alternate',
easing: 'ease-in-out',
};
const animation = element.animate(keyframes, options);
// 控制
animation.pause();
animation.play();
animation.reverse();
animation.finish();
animation.cancel();
// 事件
animation.onfinish = () => console.log('完成');性能优化
/* ========== GPU 加速 — 只动画 transform 和 opacity ========== */
/* 这些属性由合成器(Compositor)处理,不触发布局重排 */
.performant-animate {
will-change: transform, opacity;
/* 或使用 transform: translateZ(0) 强制创建 GPU 层 */
transform: translateZ(0);
}
/* ========== 避免动画这些属性(触发重排) ========== */
/* Bad — 触发 Layout
width, height, padding, margin
top, left, right, bottom
font-size, line-height
border-width, border-radius(某些浏览器)
*/
/* Good — 只触发 Composite(合成)
transform: translate/scale/rotate
opacity
filter
clip-path
*/
/* ========== will-change 最佳实践 ========== */
.animated {
/* 提前告知浏览器哪些属性将变化 */
will-change: transform;
}
/* 动画结束后移除 will-change */
.animated.animation-done {
will-change: auto;
}
/* ========== prefers-reduced-motion — 无障碍 ========== */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* ========== FLIP 动画技巧(JavaScript) ========== */
/*
FLIP: First, Last, Invert, Play
用于元素位置变化时的平滑过渡
1. First — 记录元素当前位置
2. Last — 执行 DOM 变化后记录新位置
3. Invert — 用 transform 将元素从 Last 移回 First
4. Play — 移除 transform,浏览器自动过渡到 Last
*/
function flipAnimate(elements) {
// First
const firstPositions = new Map();
elements.forEach(el => {
firstPositions.set(el, el.getBoundingClientRect());
});
// 执行 DOM 变化...
// Last + Invert + Play
elements.forEach(el => {
const first = firstPositions.get(el);
const last = el.getBoundingClientRect();
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
if (deltaX === 0 && deltaY === 0) return;
// Invert
el.style.transform = `translate(${deltaX}px, ${deltaY}px)`;
el.style.transition = 'none';
// Play
requestAnimationFrame(() => {
el.style.transition = 'transform 0.3s ease';
el.style.transform = '';
});
});
}Vue/React 中的过渡动画
<!-- Vue Transition 组件 -->
<template>
<Transition name="fade">
<div v-if="visible">内容</div>
</Transition>
<Transition name="slide">
<div v-if="visible">内容</div>
</Transition>
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</TransitionGroup>
</template>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
.slide-enter-active, .slide-leave-active { transition: transform 0.3s ease; }
.slide-enter-from { transform: translateX(-100%); }
.slide-leave-to { transform: translateX(100%); }
.list-enter-active, .list-leave-active { transition: all 0.3s ease; }
.list-enter-from { opacity: 0; transform: translateX(-20px); }
.list-leave-to { opacity: 0; transform: translateX(20px); }
.list-move { transition: transform 0.3s ease; }
</style>// React — CSS Transition
import { CSSTransition, TransitionGroup } from 'react-transition-group';
<CSSTransition in={visible} timeout={300} classNames="fade" unmountOnExit>
<div>内容</div>
</CSSTransition>
<TransitionGroup component="ul">
{items.map(item => (
<CSSTransition key={item.id} timeout={300} classNames="list">
<li>{item.name}</li>
</CSSTransition>
))}
</TransitionGroup>优点
缺点
总结
CSS transition 用于简单属性过渡(hover、状态切换),animation + @keyframes 用于复杂序列动画。优先使用 transform 和 opacity 实现动画(GPU 加速,不触发布局重排)。will-change 提前告知浏览器哪些属性将变化,但滥用会增加内存。prefers-reduced-motion 确保无障碍适配。
关键知识点
- transition 是隐式动画(两个状态间过渡),animation 是显式动画(定义关键帧)。
- 只有 transform 和 opacity 属性的动画由合成器处理,不触发布局重排。
- will-change 提前告知浏览器哪些属性将变化,但滥用会增加内存消耗。
- cubic-bezier 控制动画的缓动曲线。
项目落地视角
- 将常用动画效果封装为 CSS 工具类或 Tailwind 插件。
- 动画时长建议 200-500ms,过长影响操作效率。
- 为所有动画添加 prefers-reduced-motion 适配。
常见误区
- 动画 width/height/top/left 导致重排,应使用 transform 替代。
- 同时运行过多动画导致掉帧。
- 忽略动画结束后移除 will-change 属性。
进阶路线
- 学习 Web Animations API 用 JS 精细控制动画。
- 研究 FLIP 动画技巧。
- 了解 Lottie 和 GSAP 等高级动画库。
适用场景
- 按钮 hover 效果和页面过渡。
- 加载状态指示器。
- 列表项入场/退场动画。
落地建议
- 建立项目动画时长和缓动函数规范。
- 使用 transform 和 opacity 优先于布局属性。
- 封装常用动画为工具类。
排错清单
- 用 DevTools Animation 面板检查动画时序。
- 用 Performance 面板检查是否有布局抖动。
- 检查 will-change 是否滥用导致内存问题。
复盘问题
- 页面上同时有多少个动画在运行?是否超过 60fps 预算?
- 动画是否在低端设备上也能流畅运行?
- prefers-reduced-motion 下用户体验是否仍然完整?
