CSS 动画无需 JavaScript 即可让网页焕发生机。使用 @keyframes 和 animation 简写属性,你可以创建从微妙的淡入到复杂多步骤序列的各种效果。本完整 CSS 动画与 @keyframes 指南涵盖语法、时间函数、性能优化、无障碍访问以及数十个可直接复制使用的示例。
1. @keyframes 语法
@keyframes 规则定义动画的各个阶段。你为它指定一个名称,然后描述在动画时间线的每个点上应该应用什么 CSS 属性。浏览器会在每个定义的步骤之间平滑插值。
from / to (Two-Step)
/* Simple two-step animation using from/to */
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* from = 0%, to = 100% */
.element {
animation: fade-in 0.5s ease;
}Percentage Steps (Multi-Step)
/* Multi-step animation with percentage keyframes */
@keyframes bounce {
0% {
transform: translateY(0);
}
25% {
transform: translateY(-30px);
}
50% {
transform: translateY(0);
}
75% {
transform: translateY(-15px);
}
100% {
transform: translateY(0);
}
}
/* You can combine from/to with percentages */
@keyframes color-shift {
0%, 100% { background-color: #3b82f6; }
25% { background-color: #8b5cf6; }
50% { background-color: #ec4899; }
75% { background-color: #f59e0b; }
}Multiple Properties in Keyframes
@keyframes slide-in-fade {
0% {
opacity: 0;
transform: translateX(-100px);
}
60% {
opacity: 1;
transform: translateX(10px);
}
100% {
opacity: 1;
transform: translateX(0);
}
}命名约定:使用小写连字符命名法(例如 fade-in、slide-up)。避免使用 animation1 这样的通用名称。名称区分大小写,且不能与 CSS 关键字(如 none、inherit 或 initial)冲突。
2. animation 简写属性
animation 简写属性将所有动画子属性合并为一个声明。持续时间和延迟之间的区分取决于顺序(第一个时间值是持续时间,第二个是延迟),其他属性顺序灵活。
各个子属性包括:animation-name(名称)、animation-duration(持续时间)、animation-timing-function(时间函数)、animation-delay(延迟)、animation-iteration-count(迭代次数)、animation-direction(方向)、animation-fill-mode(填充模式)和 animation-play-state(播放状态)。
/* animation shorthand syntax */
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
/* Examples */
animation: fade-in 0.3s ease;
animation: slide-up 0.5s ease-out 0.2s;
animation: spin 1s linear infinite;
animation: bounce 0.6s ease-in-out 0s infinite alternate;
animation: fade-in 0.5s ease forwards;
/* Individual properties (equivalent to above) */
.element {
animation-name: fade-in;
animation-duration: 0.5s;
animation-timing-function: ease;
animation-delay: 0s;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
/* Multiple animations on one element */
.element {
animation:
fade-in 0.5s ease,
slide-up 0.5s ease 0.1s,
color-shift 3s linear infinite;
}
/* fill-mode values */
animation-fill-mode: none; /* default: no styles applied outside animation */
animation-fill-mode: forwards; /* retains the final keyframe styles */
animation-fill-mode: backwards; /* applies first keyframe styles during delay */
animation-fill-mode: both; /* combines forwards + backwards */
/* direction values */
animation-direction: normal; /* 0% -> 100% */
animation-direction: reverse; /* 100% -> 0% */
animation-direction: alternate; /* 0% -> 100% -> 0% -> ... */
animation-direction: alternate-reverse; /* 100% -> 0% -> 100% -> ... */3. 时间函数(Timing Functions)
时间函数控制动画的加速曲线,决定关键帧之间中间值的计算方式,赋予动画独特的节奏和感觉。
/* Built-in keyword timing functions */
animation-timing-function: ease; /* default: slow start, fast, slow end */
animation-timing-function: linear; /* constant speed */
animation-timing-function: ease-in; /* slow start, fast end */
animation-timing-function: ease-out; /* fast start, slow end */
animation-timing-function: ease-in-out; /* slow start and end */
/* Equivalent cubic-bezier() values */
ease: cubic-bezier(0.25, 0.1, 0.25, 1.0)
linear: cubic-bezier(0.0, 0.0, 1.0, 1.0)
ease-in: cubic-bezier(0.42, 0.0, 1.0, 1.0)
ease-out: cubic-bezier(0.0, 0.0, 0.58, 1.0)
ease-in-out: cubic-bezier(0.42, 0.0, 0.58, 1.0)
/* Custom cubic-bezier examples */
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55); /* back-in-out */
animation-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* overshoot */
animation-timing-function: cubic-bezier(0.22, 0.61, 0.36, 1); /* ease-out-cubic */cubic-bezier() 接受四个值(x1, y1, x2, y2)定义三次贝塞尔曲线的控制点。y 轴超出 0-1 范围的值会产生过冲/弹跳效果。使用浏览器开发者工具或在线工具可视化曲线。
/* steps() function for frame-by-frame animations */
animation-timing-function: steps(4); /* 4 equal jumps, default jump-end */
animation-timing-function: steps(4, jump-start); /* jump at start of each interval */
animation-timing-function: steps(4, jump-end); /* jump at end of each interval */
animation-timing-function: steps(4, jump-both); /* jump at start and end */
animation-timing-function: steps(4, jump-none); /* no jump at start or end */
animation-timing-function: step-start; /* = steps(1, jump-start) */
animation-timing-function: step-end; /* = steps(1, jump-end) */
/* Sprite sheet animation example */
.sprite {
width: 64px;
height: 64px;
background: url('spritesheet.png') 0 0;
animation: walk 0.8s steps(8) infinite;
}
@keyframes walk {
to { background-position: -512px 0; } /* 64px * 8 frames = 512px */
}steps() 创建阶梯函数,在状态之间跳跃而非平滑插值。可选的第二个参数(jump-start、jump-end、jump-both、jump-none)控制跳跃发生的时机。这对精灵图动画至关重要。
4. 常用动画(复制即用)
这些是 Web 开发中最常用的动画效果。每个示例都可以直接用于生产环境——只需复制 CSS 并将类名应用到你的元素上。
Fade In
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-in {
animation: fadeIn 0.5s ease forwards;
}Fade Out
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
.fade-out {
animation: fadeOut 0.5s ease forwards;
}Slide In from Left
@keyframes slideInLeft {
from {
opacity: 0;
transform: translateX(-60px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.slide-in-left {
animation: slideInLeft 0.5s ease-out forwards;
}Slide In from Bottom
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(40px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-in-up {
animation: slideInUp 0.5s ease-out forwards;
}Bounce
@keyframes bounce {
0%, 20%, 50%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-30px);
}
60% {
transform: translateY(-15px);
}
}
.bounce {
animation: bounce 1s ease infinite;
}Pulse
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
.pulse {
animation: pulse 2s ease-in-out infinite;
}Spin
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spin {
animation: spin 1s linear infinite;
}Shake
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
.shake {
animation: shake 0.6s ease-in-out;
}
/* Trigger shake on invalid input */
input:invalid:focus {
animation: shake 0.4s ease-in-out;
}5. Transform 变换动画
transform 属性非常适合做动画,因为它由 GPU 加速且不会触发布局重新计算。你可以在一个声明中组合多个变换函数来实现复杂的运动效果。
Rotate
@keyframes rotate360 {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Pendulum swing */
@keyframes pendulum {
0% { transform: rotate(-30deg); }
50% { transform: rotate(30deg); }
100% { transform: rotate(-30deg); }
}
.pendulum {
transform-origin: top center;
animation: pendulum 2s ease-in-out infinite;
}Scale
/* Pop-in effect */
@keyframes popIn {
0% {
transform: scale(0);
opacity: 0;
}
70% {
transform: scale(1.1);
opacity: 1;
}
100% {
transform: scale(1);
}
}
.pop-in {
animation: popIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55) forwards;
}
/* Heartbeat */
@keyframes heartbeat {
0%, 100% { transform: scale(1); }
14% { transform: scale(1.3); }
28% { transform: scale(1); }
42% { transform: scale(1.3); }
70% { transform: scale(1); }
}
.heartbeat {
animation: heartbeat 1.5s ease-in-out infinite;
}Translate
/* Floating effect */
@keyframes float {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-20px); }
}
.float {
animation: float 3s ease-in-out infinite;
}
/* Marquee scroll */
@keyframes marquee {
from { transform: translateX(100%); }
to { transform: translateX(-100%); }
}
.marquee {
animation: marquee 10s linear infinite;
}Skew
@keyframes skewShake {
0%, 100% { transform: skewX(0deg); }
25% { transform: skewX(5deg); }
75% { transform: skewX(-5deg); }
}
.skew-shake {
animation: skewShake 0.5s ease-in-out;
}Combined Transforms
/* Rotate + Scale + Translate combined */
@keyframes complexEntry {
0% {
transform: translateY(50px) rotate(-10deg) scale(0.8);
opacity: 0;
}
100% {
transform: translateY(0) rotate(0deg) scale(1);
opacity: 1;
}
}
.complex-entry {
animation: complexEntry 0.6s cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
}
/* 3D flip */
@keyframes flip {
0% { transform: perspective(600px) rotateY(0deg); }
100% { transform: perspective(600px) rotateY(360deg); }
}
.flip {
animation: flip 1s ease-in-out;
}6. JavaScript 中的动画事件
CSS 动画触发三个可以用 JavaScript 监听的事件:animationstart(开始)、animationend(结束)和 animationiteration(每次迭代)。这些事件让你可以将 JavaScript 逻辑与 CSS 动画状态同步。
const element = document.querySelector('.animated');
// Fires when the animation starts (after any delay)
element.addEventListener('animationstart', (e) => {
console.log(`Animation "${e.animationName}" started`);
console.log(`Duration: ${e.elapsedTime}s`);
});
// Fires when the animation completes
element.addEventListener('animationend', (e) => {
console.log(`Animation "${e.animationName}" ended`);
// Common pattern: remove the animation class after completion
element.classList.remove('animate');
});
// Fires at the end of each iteration (except the last)
element.addEventListener('animationiteration', (e) => {
console.log(`Animation "${e.animationName}" completed iteration`);
console.log(`Elapsed time: ${e.elapsedTime}s`);
});Practical Example: One-Shot Animation
/* CSS */
.notification-enter {
animation: slideInRight 0.4s ease-out forwards;
}
@keyframes slideInRight {
from { transform: translateX(100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* JavaScript: remove class after animation so it can replay */
document.querySelectorAll('.notification').forEach(el => {
el.addEventListener('animationend', () => {
el.classList.remove('notification-enter');
});
});
/* Re-trigger: remove and re-add class with a reflow in between */
function triggerAnimation(el) {
el.classList.remove('notification-enter');
// Force reflow to restart animation
void el.offsetWidth;
el.classList.add('notification-enter');
}注意:如果动画在完成前被移除(例如移除了类名),animationend 事件不会触发。animationiteration 事件在每次迭代结束时触发,但最后一次迭代除外。
7. Transition 与 Animation 对比
CSS 过渡和动画都能创建运动效果,但它们有不同的用途和能力。理解何时使用哪个对于编写整洁、可维护的 CSS 至关重要。
| Feature | transition | animation |
|---|---|---|
| Trigger required | Yes (:hover, :focus, class change) | No (auto-plays) |
| Number of states | 2 (start + end) | Unlimited (@keyframes) |
| Looping | No | Yes (infinite) |
| Direction control | Auto-reverses on state change | normal, reverse, alternate |
| Play state | No pause/resume | paused / running |
| Fill mode | Implicit (stays in end state) | none, forwards, backwards, both |
| JS Events | transitionend | animationstart, animationend, animationiteration |
| Best for | Hover effects, toggles, interactive UI | Loading states, attention seekers, complex sequences |
/* TRANSITION: hover color change */
.button {
background: #3b82f6;
transition: background 0.3s ease, transform 0.2s ease;
}
.button:hover {
background: #2563eb;
transform: translateY(-2px);
}
/* ANIMATION: autonomous pulsing glow */
@keyframes glow {
0%, 100% { box-shadow: 0 0 5px #3b82f6; }
50% { box-shadow: 0 0 20px #3b82f6, 0 0 40px #3b82f680; }
}
.notification-badge {
animation: glow 2s ease-in-out infinite;
}经验法则:对用户交互触发的简单状态变化(hover、focus、class 切换)使用 transition。对复杂的、自主的、多步骤的或循环的运动使用 animation。
8. 性能优化
并非所有 CSS 属性动画的性能都相同。有些会触发昂贵的布局重新计算,而有些完全由 GPU 合成器处理。理解这个区别是实现流畅 60fps 动画的关键。
GPU 加速属性:transform 和 opacity 在 GPU 上合成,跳过布局和绘制阶段。动画应始终优先使用这两个属性。
/* GOOD: GPU-accelerated, no layout or paint */
@keyframes slide {
from { transform: translateX(-100%); opacity: 0; }
to { transform: translateX(0); opacity: 1; }
}
/* BAD: triggers layout on every frame */
@keyframes slide-bad {
from { left: -100%; opacity: 0; }
to { left: 0; opacity: 1; }
}will-change:此属性向浏览器提示元素将被动画化,使其可以提前优化。但要谨慎使用——对过多元素应用 will-change 会浪费 GPU 内存。
/* Apply will-change before animation starts */
.card {
will-change: transform;
transition: transform 0.3s ease;
}
.card:hover {
transform: translateY(-5px);
}
/* Remove will-change after animation completes */
.animated-element {
will-change: transform, opacity;
}
.animated-element.done {
will-change: auto; /* release GPU memory */
}避免动画化:width、height、top、left、margin、padding、border-width、font-size。这些会触发布局(回流),是最昂贵的渲染操作。用 transform: translate() 代替 top/left,用 transform: scale() 代替 width/height。
/* INSTEAD OF animating width: */
/* Bad */
@keyframes expand-bad {
from { width: 0; }
to { width: 200px; }
}
/* Good: use scaleX */
@keyframes expand-good {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
.expand {
transform-origin: left center;
animation: expand-good 0.5s ease forwards;
}
/* INSTEAD OF animating top/left: */
/* Bad */
@keyframes move-bad {
from { top: 0; left: 0; }
to { top: 100px; left: 200px; }
}
/* Good: use translate */
@keyframes move-good {
from { transform: translate(0, 0); }
to { transform: translate(200px, 100px); }
}9. prefers-reduced-motion(无障碍访问)
部分用户会因动画而感到眩晕、癫痫发作或不适。prefers-reduced-motion 媒体查询让你可以尊重他们的系统偏好设置。这不是可选的——它是 WCAG 2.1(成功标准 2.3.3)下的无障碍要求。
有两种方法:(1)为偏好减少动效的用户移除动画,或(2)仅为没有偏好的用户添加动画。第二种方法(渐进增强)被认为更安全。
Approach 1: Remove Animations
/* Remove all animations and transitions for users who prefer 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;
}
}Approach 2: Progressive Enhancement (Recommended)
/* Default: no animation */
.card {
opacity: 1;
transform: none;
}
/* Only animate if user has no motion preference */
@media (prefers-reduced-motion: no-preference) {
.card {
animation: fadeInUp 0.5s ease forwards;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
}JavaScript Detection
// Check if user prefers reduced motion
const prefersReducedMotion = window.matchMedia(
'(prefers-reduced-motion: reduce)'
).matches;
if (prefersReducedMotion) {
// Skip animation, show content immediately
element.style.opacity = '1';
} else {
element.classList.add('animate-in');
}
// Listen for changes (user toggles setting)
window.matchMedia('(prefers-reduced-motion: reduce)')
.addEventListener('change', (e) => {
if (e.matches) {
document.body.classList.add('reduce-motion');
} else {
document.body.classList.remove('reduce-motion');
}
});10. 加载旋转器(纯 CSS)
加载旋转器是 CSS 动画最常见的应用之一。以下是 5 种不同风格,全部使用纯 CSS 实现,无需 JavaScript 或图片。
1. Classic Border Spinner
.spinner-border {
width: 40px;
height: 40px;
border: 4px solid rgba(59, 130, 246, 0.2);
border-top-color: #3b82f6;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}2. Dual Ring Spinner
.spinner-dual {
width: 40px;
height: 40px;
border: 4px solid transparent;
border-top-color: #3b82f6;
border-bottom-color: #8b5cf6;
border-radius: 50%;
animation: spin 1s linear infinite;
}3. Pulsing Dot
.spinner-pulse {
width: 20px;
height: 20px;
background: #3b82f6;
border-radius: 50%;
animation: pulse-spinner 1.2s ease-in-out infinite;
}
@keyframes pulse-spinner {
0%, 100% {
transform: scale(0.5);
opacity: 0.5;
}
50% {
transform: scale(1);
opacity: 1;
}
}4. Three Bouncing Dots
.spinner-dots {
display: flex;
gap: 6px;
}
.spinner-dots span {
width: 10px;
height: 10px;
background: #3b82f6;
border-radius: 50%;
animation: dot-bounce 1.4s ease-in-out infinite;
}
.spinner-dots span:nth-child(1) { animation-delay: 0s; }
.spinner-dots span:nth-child(2) { animation-delay: 0.2s; }
.spinner-dots span:nth-child(3) { animation-delay: 0.4s; }
@keyframes dot-bounce {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
/* HTML: <div class="spinner-dots"><span></span><span></span><span></span></div> */5. Rotating Squares
.spinner-squares {
width: 40px;
height: 40px;
position: relative;
}
.spinner-squares::before,
.spinner-squares::after {
content: '';
position: absolute;
inset: 0;
border-radius: 4px;
background: #3b82f6;
}
.spinner-squares::before {
animation: square-rotate 2s ease infinite;
}
.spinner-squares::after {
animation: square-rotate 2s ease infinite 1s;
opacity: 0.5;
}
@keyframes square-rotate {
0% { transform: rotate(0deg) scale(1); }
25% { transform: rotate(90deg) scale(0.6); }
50% { transform: rotate(180deg) scale(1); }
75% { transform: rotate(270deg) scale(0.6); }
100% { transform: rotate(360deg) scale(1); }
}11. 滚动触发动画
滚动触发动画在用户向下滚动页面时逐步展示内容。现代方法使用 Intersection Observer API 在元素进入视口时添加 CSS 类,将动画逻辑保留在 CSS 中,触发逻辑使用最少的 JavaScript。
Intersection Observer + CSS Classes
/* CSS: define the animation and the initial hidden state */
.reveal {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.reveal.visible {
opacity: 1;
transform: translateY(0);
}
/* Staggered children */
.reveal.visible .child:nth-child(1) { transition-delay: 0s; }
.reveal.visible .child:nth-child(2) { transition-delay: 0.1s; }
.reveal.visible .child:nth-child(3) { transition-delay: 0.2s; }// JavaScript: Intersection Observer
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
// Optional: stop observing after first trigger
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.1, // Trigger when 10% visible
rootMargin: '0px 0px -50px 0px' // Offset trigger point
}
);
// Observe all elements with the .reveal class
document.querySelectorAll('.reveal').forEach((el) => {
observer.observe(el);
});CSS Scroll-Driven Animations (Chrome / Edge)
/* Pure CSS scroll-triggered animation (no JavaScript!) */
/* Supported in Chrome 115+ and Edge 115+ */
@keyframes reveal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.scroll-reveal {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
/* Scroll progress bar at top of page */
.scroll-progress {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
transform-origin: left;
animation: scaleProgress linear;
animation-timeline: scroll();
}
@keyframes scaleProgress {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}纯 CSS 方案:新的 animation-timeline: scroll() 和 animation-timeline: view() 属性已在 Chrome 和 Edge 中支持,允许无需任何 JavaScript 实现滚动驱动动画。
12. 常见问题
CSS transition 和 CSS animation 有什么区别?
CSS 过渡需要触发器(如 :hover 或类名变化),且只能在两个状态(起始和结束)之间动画。CSS 动画使用 @keyframes 可以自动运行、无限循环、定义多个中间步骤、反向播放以及暂停/恢复。简单的交互状态变化用 transition;复杂的、自主的或多步骤效果用 animation。
如何让 CSS 动画无限循环?
设置 animation-iteration-count: infinite 或使用简写:animation: spin 1s linear infinite。动画将无限期重复,直到元素被移除或 animation 属性被更改。你还可以使用 JavaScript 的 animationiteration 事件在每次循环时执行操作。
为什么我的 CSS 动画卡顿或不流畅?
动画卡顿通常是因为对触发布局的属性(width、height、top、left、margin)或绘制的属性(background-color、box-shadow、border-radius)做了动画。应改用 transform 和 opacity,它们在 GPU 上合成。同时给动画元素添加 will-change: transform,并确保不要同时动画化太多元素。
如何暂停和恢复 CSS 动画?
使用 animation-play-state 属性。设为 paused 可在当前位置冻结动画,设为 running 可恢复播放。示例:.paused { animation-play-state: paused; }。你可以用 JavaScript 通过添加/移除类来切换。
如何在元素滚动到视口时触发 CSS 动画?
使用 JavaScript 的 Intersection Observer API 检测元素何时进入视口,然后添加一个启动动画的 CSS 类。在类上定义动画(例如 .animate-in { animation: fadeIn 0.6s ease forwards; }),并在观察到元素时添加该类。对于现代浏览器(Chrome/Edge),还可以使用原生 CSS animation-timeline: view() 属性实现纯 CSS 方案。
CSS 动画是增强用户体验的强大、高性能方式。结合无障碍实践和 GPU 优化属性,它们可以让任何 Web 应用看起来精致而专业。