
做动画这件事听起来简单真正落到项目里却常常遇到“动画没效果”“动画很卡”“动画结束闪回原点”这类问题。本文整理了一套从 CSS3 基础语法到完整业务案例的前端动画实战笔记覆盖 transition、transform、keyframes、滚动联动、性能优化和常见排错适合刚接触前端动画的初学者也适合需要快速搭出一套可维护动效方案的进阶开发者。先明确一点本文讲的“做动画”主要指 Web 前端中的 CSS3 动画与浏览器动画实现方案不涉及建模软件、视频剪辑或 3D 渲染。前端动画的本质是在连续的时间片内不断改变元素的视觉属性让用户产生“动起来”的感知。理解了这一点后面所有的代码都是在回答两个问题改什么属性、按什么节奏改。1. 背景与核心概念1.1 动画的本质无论使用哪种技术方案前端动画的核心都是“属性随时间变化”。比如一个按钮从红色变成蓝色一张图片从透明到完全不透明一个弹窗从屏幕顶部滑入中间这些都是视觉属性的连续变化。在浏览器里实现这种变化主要有三种手段方案原理适用场景CSS transition属性值从一个状态过渡到另一个状态hover 效果、展开收起、弹窗淡入CSS animation通过 keyframes 定义多阶段动画loading、循环动效、复杂关键帧动画JavaScript 动画用 JS 控制属性变化或驱动游戏循环交互复杂、数据驱动、Canvas 动画CSS 动画的优点是写法简单、浏览器原生优化、不占用主线程逻辑JavaScript 动画的优点是控制力强能根据用户交互动态计算。实际项目中两者通常配合使用而不是互相对立。1.2 CSS 动画与 JS 动画怎么选很多新手纠结“到底用 CSS 还是用 JS”这里给一个相对简单的判断标准如果动画是触发式的比如鼠标悬停、点击切换、类名变化优先用 CSS transition。如果动画是循环播放的比如 loading 转圈、轮播图自动播放优先用 CSS animation。如果动画状态依赖数据比如进度条根据接口返回值变化优先用 JS 配合 CSS 变量。如果动画需要帧级控制、拖拽交互、物理模拟建议使用 JavaScript 动画库比如 GSAP、Popmotion或者直接操作 Canvas。CSS 能解决的问题不要急着引入动画库。减少依赖体积也减少调试成本。1.3 常见应用场景前端动画的应用场景非常广泛常见的有页面加载反馈loading 转圈、骨架屏、进度条。交互反馈按钮点击波纹、菜单展开收起、弹窗淡入淡出。视觉引导新功能提示、滚动入场动画、图标变换。数据可视化图表过渡、数字滚动、排行榜变动。本文后面的实战案例会围绕 loading、卡片悬浮、滚动入场这三个最常用的场景展开每一个都能直接复制到项目中使用。2. 环境准备与项目结构2.1 开发环境本文的示例不需要安装任何框架依赖只需要一个现代浏览器和任意代码编辑器即可。操作系统Windows / macOS / Linux 均可。浏览器建议使用 Chrome 或 Edge方便打开 DevTools 的 Performance 面板检查动画性能。编辑器VS Code、WebStorm 均可本文不依赖编辑器插件。运行方式直接双击 HTML 文件在浏览器打开或使用 VS Code 的 Live Server 插件启动本地服务。版本方面不需要刻意统一CSS3 动画相关语法已经稳定多年主流浏览器均支持。需要特别注意的是 CSS 属性前缀问题现代浏览器已经不需要手动写-webkit-前缀但如果你要兼容旧版本浏览器建议通过 Autoprefixer 自动处理。2.2 示例项目结构为了清晰演示本文创建一个最小的项目结构animation-demo/ ├── case1-loading.html ├── case2-card.html ├── case3-scroll.html └── README.md这种方式适合零基础读者每个案例一个独立 HTML 文件互不干扰。实际项目开发中建议把公共动画样式抽成独立的 CSS 文件例如animations.css再按业务组件拆分布局样式。3. CSS3 动画核心语法拆解3.1 transition 过渡最简单的“做动画”方式transition 是最容易上手的动画语法它描述的是“当某个属性发生变化时如何平滑过去”。先看一个最简单的例子。下面这段代码让按钮在鼠标悬停时背景颜色平滑变化!DOCTYPE html html langzh-CN head meta charsetUTF-8 titletransition 基础示例/title style .btn { width: 120px; height: 44px; background: #409eff; color: #fff; border: none; border-radius: 6px; cursor: pointer; transition: background-color 0.3s ease; } .btn:hover { background: #337ecc; } /style /head body button classbtn悬停试试/button /body /html关键代码是这一行transition: background-color 0.3s ease;它拆开来看包含四个部分transition-property要过渡的属性名这里是background-color。transition-duration过渡耗时这里是0.3s。transition-timing-function速度曲线常用ease、linear、ease-in、ease-out。transition-delay延迟开始时间默认 0。新手容易犯的一个错误是写成transition: all 0.3s虽然能用但all会让所有可过渡属性都参与过渡一旦元素属性变多可能出现意料之外的动画效果也会增加浏览器计算压力。推荐明确写出需要过渡的属性名。transition 只能响应属性的“开始状态”和“结束状态”中间过程由浏览器自动插值计算。它无法实现多阶段动画比如“先放大、再缩小、最后旋转”这种需求要交给 animation。3.2 transform 变换让元素动起来的核心属性动画如果只改颜色和透明度表现力远远不够。真正让元素“移动”“旋转”“缩放”的是transform属性。transform 常用函数如下函数作用示例translateX / translateY平移translateX(50px)scale缩放scale(1.2)rotate旋转rotate(45deg)skew倾斜skew(10deg)translate3d / scale3d3D 变换translate3d(10px, 0, 0)一个典型的卡片悬浮效果就是通过transform: translateY()和scale()结合完成的.card { transition: transform 0.3s ease, box-shadow 0.3s ease; } .card:hover { transform: translateY(-8px) scale(1.02); box-shadow: 0 12px 24px rgba(0, 0, 0, 0.12); }这里需要强调一个性能相关的点transform和opacity是浏览器专门优化的合成器属性修改它们不会触发重排和重绘动画过程可以交给 GPU 合成性能远好于修改width、height、top、left等布局属性。后面的性能章节还会详细展开。3.3 animation 与 keyframes实现多阶段动画当动画不止“开始”和“结束”两个状态时就需要使用keyframes定义关键帧再用animation属性引用。先看一个经典的多阶段动画一个方块先向右移动再旋转最后变成圆形。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titlekeyframes 基础示例/title style .box { width: 80px; height: 80px; background: #409eff; border-radius: 8px; animation: demoMove 2s ease-in-out infinite alternate; } keyframes demoMove { 0% { transform: translateX(0); border-radius: 8px; } 50% { transform: translateX(120px) rotate(180deg); } 100% { transform: translateX(240px) rotate(360deg); border-radius: 50%; } } /style /head body div classbox/div /body /htmlkeyframes内部用百分比定义每个时间点的状态0%代表动画起点。50%代表动画进行到一半时的状态。100%代表动画终点。如果动画只想定义起点和终点也可以简写成from和tokeyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }3.4 animation 的 8 个子属性animation是复合属性由 8 个子属性组成写作顺序自由但推荐固定顺序便于阅读.element { animation: animation-name /* 动画名称对应 keyframes 名字 */ animation-duration /* 动画时长 */ animation-timing-function /* 速度曲线 */ animation-delay /* 延迟时间 */ animation-iteration-count /* 播放次数 */ animation-direction /* 播放方向 */ animation-fill-mode /* 结束后状态 */ animation-play-state; /* 播放/暂停 */ }一个典型写法.spinner { animation: spin 1s linear infinite; }这里省略了delay、direction、fill-mode、play-state浏览器会取默认值。常用子属性含义animation-iteration-count播放次数写成infinite表示无限循环。animation-directionnormal正向播放alternate正反交替播放。animation-fill-modeforwards保持最后一帧状态backwards在延迟期间应用第一帧状态both同时应用前后两帧。animation-play-staterunning播放中paused暂停。新手最常踩的坑是动画播完后元素瞬间回到初始状态。想要让动画停在最终位置必须显式设置animation-fill-mode: forwards或用animation: ... both。4. 完整实战三个贴近业务的动画案例4.1 案例一纯 CSS Loading 加载动画加载动画是每个前端项目几乎都会遇到的需求。用一个转圈效果举例完整代码可以直接保存为case1-loading.html。!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleCSS Loading 动画/title style .loading { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 100vh; } .spinner { width: 48px; height: 48px; border: 4px solid rgba(64, 158, 255, 0.2); border-top-color: #409eff; border-radius: 50%; animation: spin 0.8s linear infinite; } keyframes spin { to { transform: rotate(360deg); } } .loading-text { margin-top: 16px; color: #666; font-size: 14px; } /style /head body div classloading div classspinner/div p classloading-text加载中请稍候.../p /div /body /html实现思路创建一个宽高相等的圆角元素。只给一个边框设置深色其余边框设置为浅色透明。让整个元素无限旋转形成转圈效果。为什么推荐用border-radius: 50%加border而不是用图片素材因为纯 CSS 实现的 loading 不依赖外部资源静态页面也能展示也方便通过 CSS 变量调整颜色和尺寸。另一种常见的 loading 是圆点跳动效果可以同时使用多个子元素给每个子元素设置不同的animation-delay形成波浪感!DOCTYPE html html langzh-CN head meta charsetUTF-8 titleLoading 点阵动画/title style .dots { display: flex; gap: 8px; min-height: 100vh; align-items: center; justify-content: center; } .dots span { width: 12px; height: 12px; background: #409eff; border-radius: 50%; animation: bounce 0.6s ease-in-out infinite alternate; } .dots span:nth-child(2) { animation-delay: 0.2s; } .dots span:nth-child(3) { animation-delay: 0.4s; } keyframes bounce { from { transform: translateY(0); opacity: 0.5; } to { transform: translateY(-14px); opacity: 1; } } /style /head body div classdots span/span span/span span/span /div /body /html注意这里用了alternate方向让圆点在上下之间往返配合不同的animation-delay三个点的节奏错开视觉上更活泼。4.2 案例二hover 悬浮卡片动效卡片交互是后台管理系统、官网展示页都高频出现的场景。下面实现一个完整可运行的卡片悬浮动效保存为case2-card.html。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title悬浮卡片动效/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { display: flex; align-items: center; justify-content: center; min-height: 100vh; background: #f5f7fa; font-family: Arial, sans-serif; } .card { width: 260px; padding: 20px; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); transition: transform 0.3s ease, box-shadow 0.3s ease; } .card:hover { transform: translateY(-8px); box-shadow: 0 16px 32px rgba(0, 0, 0, 0.12); } .card:focus-within { transform: translateY(-8px); box-shadow: 0 16px 32px rgba(0, 0, 0, 0.12); } .card-image { width: 100%; height: 140px; background: linear-gradient(135deg, #409eff, #79bbff); border-radius: 8px; transition: transform 0.4s ease; } .card:hover .card-image { transform: scale(1.03); } .card-title { margin-top: 16px; font-size: 18px; color: #303133; } .card-desc { margin-top: 8px; font-size: 14px; line-height: 1.6; color: #909399; } .card-btn { display: inline-block; margin-top: 16px; padding: 8px 20px; font-size: 14px; color: #fff; background: #409eff; border: none; border-radius: 6px; cursor: pointer; transition: background-color 0.2s ease; } .card-btn:hover { background: #337ecc; } /style /head body div classcard div classcard-image/div h3 classcard-title前端动画实战/h3 p classcard-desc 从 CSS3 基础语法到业务场景落地包含 loading、悬浮卡片、滚动入场等常见动效方案。 /p button classcard-btn马上学习/button /div /body /html这个案例有几个设计细节值得注意卡片悬浮的位移和阴影使用两个独立属性的 transition 组合实现“浮起来”的层次感。图片区域单独做了scale(1.03)缩放视觉上比纯文字卡片更有反馈。加入了:focus-within伪类当用户通过 Tab 键聚焦到卡片的按钮时也有同样的悬浮效果。这是容易被忽略的可访问性优化。4.3 案例三滚动入场动画与 JS 联动纯 CSS 动画只能自己播放如果想让动画“等用户滚动到某个位置才开始”需要配合 JavaScript 判断元素是否进入视口。这里使用浏览器原生IntersectionObserver不需要引入任何第三方库。完整代码保存为case3-scroll.html。!DOCTYPE html html langzh-CN head meta charsetUTF-8 title滚动入场动画/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #f5f7fa; font-family: Arial, sans-serif; } .section { min-height: 80vh; display: flex; align-items: center; justify-content: center; } .card { width: 300px; padding: 24px; background: #fff; border-radius: 12px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); opacity: 0; transform: translateY(40px); transition: opacity 0.6s ease, transform 0.6s ease; } .card.visible { opacity: 1; transform: translateY(0); } .card h2 { font-size: 20px; color: #303133; margin-bottom: 8px; } .card p { font-size: 14px; line-height: 1.6; color: #909399; } media (prefers-reduced-motion: reduce) { .card { transition: none; opacity: 1; transform: none; } } /style /head body div classsection div classcard h2第一屏内容/h2 p往下滚动可以看到后面的卡片依次淡入。/p /div /div div classsection div classcard h2第二屏内容/h2 p这个卡片会在进入视口时触发入场动画。/p /div /div div classsection div classcard h2第三屏内容/h2 p整个过程完全由原生 API 实现。/p /div /div script const cards document.querySelectorAll(.card); if (IntersectionObserver in window) { const observer new IntersectionObserver( (entries) { entries.forEach((entry) { if (entry.isIntersecting) { entry.target.classList.add(visible); observer.unobserve(entry.target); } }); }, { threshold: 0.2 } ); cards.forEach((card) observer.observe(card)); } else { cards.forEach((card) card.classList.add(visible)); } /script /body /html实现思路拆解卡片默认状态是opacity: 0; transform: translateY(40px)不可见且向下偏移。当IntersectionObserver检测到卡片进入视口且可见比例达到 20% 时给卡片加上visible类。.card.visible把透明度改为 1位移改为 0触发 transition 平滑入场。动画触发后调用observer.unobserve(entry.target)避免重复触发。对于不支持IntersectionObserver的旧浏览器做降级处理所有卡片直接可见。这是一个非常标准的“滚动加载动效”方案不依赖 jQuery不依赖滚动事件的频繁计算性能表现也很稳定。5. 动画性能为什么有的动画会卡顿5.1 浏览器的渲染流程做动画不能只看效果还要关注性能。浏览器从代码到屏幕显示大致经历五个阶段JavaScript 执行处理脚本、事件、动画逻辑。Style 计算根据 CSS 规则计算每个元素的最终样式。Layout 布局计算元素的位置和尺寸。Paint 绘制把元素绘制成像素层。Composite 合成把各层合成后输出到屏幕。如果动画修改了width、height、left、top、margin等布局属性浏览器必须重新执行 Layout、Paint、Composite开销很大。频繁触发就会导致页面掉帧、卡顿。而修改transform和opacity时浏览器可以直接进入合成阶段性能要好得多。所以最佳实践是能平移的地方用translateX/translateY不要用left/top能缩放的地方用scale不要改width/height。5.2 优先使用 transform 和 opacity来看一个典型对比。假设要让一个元素从左边滑入/* 不推荐修改 left 触发布局 */ .box { position: absolute; left: 0; transition: left 0.3s ease; } .box.active { left: 200px; } /* 推荐使用 transform不影响布局 */ .box { position: absolute; transform: translateX(0); transition: transform 0.3s ease; } .box.active { transform: translateX(200px); }两种写法视觉上可能看不出差别但渲染性能完全不同。第一种写法如果同一时刻有多个元素在动画很可能出现卡顿第二种写法可以充分利用 GPU 合成。5.3 will-change 与 contain 的正确使用will-change可以提前告诉浏览器某个属性即将变化让浏览器预先分配优化资源.card { will-change: transform, opacity; }但需要注意will-change不能滥用。如果给大量元素设置will-change浏览器要维护大量合成层内存占用上升反而拖慢性能。合理的做法是只在动画即将开始前添加动画结束后移除。对于实际项目先不加will-change测一测确实出现性能问题再针对性地加这是更稳妥的思路。另外为了减少不必要的渲染范围还可以考虑 CSScontain属性不过它的使用条件比较严格在动画场景中一般不作为首选方案这里不再展开。6. 常见问题与排查思路做动画过程中难免遇到各种奇怪现象。下面把高频问题整理成表格再对部分问题做详细说明。问题现象常见原因解决思路动画没有生效属性名称写错或元素初始状态与目标状态相同检查 CSS 拼写对比状态差异动画播完闪回原样缺少 animation-fill-mode添加forwards或使用both动画只播放一次未设置 infinite 或设计就是一次根据需求设置animation-iteration-counthover 时元素抖动位移与缩放叠加导致 hover 边界变化使用稳定的容器包裹避免 hover 改变布局尺寸滚动入场动画提前触发threshold 设置太小调大threshold如 0.3 或 0.5动画卡顿掉帧动画了布局属性或同时动画太多元素改用 transform/opacity减少合成层数量页面滚动时 fixed 元素动画位移错乱对 fixed 元素使用 transform 触发新的包含块将动画拆到内部子元素避免直接 transform 父级用户开启“减弱动态效果”后仍播放未处理 prefers-reduced-motion用媒体查询关闭或简化动画6.1 为什么动画没有生效动画不生效是最常见的入门问题原因通常有三个第一keyframes的名称与animation-name不一致。CSS 是弱校验语言名称不匹配不会报错动画就是静默不执行。/* 错误名称不一致 */ .animation-name: demo; keyframes myDemo { ... }第二动画属性本身没有变化空间。比如元素默认opacity: 1动画也是从opacity: 1到opacity: 1看起来自然没有效果。第三元素被display: none隐藏或者被其他元素遮挡。建议先打开 DevTools确认元素真的在页面上并且样式计算中确实包含动画相关属性。6.2 animation-fill-mode 造成的“闪回”很多人第一次用 animation 时发现动画结束后元素“跳”回初始位置很影响体验。原因是没有设置animation-fill-mode。默认情况下动画结束的一瞬间元素会恢复成没有应用动画时的样式。要保留动画结束后的状态可以这样写.element { animation: fadeInUp 0.6s ease-out both; }both是forwards和backwards的组合延迟期间应用第一帧结束时保持最后一帧。对于大多数入场动画直接写both最省心。6.3 hover 抖动问题悬浮卡片加上translateY(-8px)和scale(1.02)后鼠标快速划过边缘时元素可能会轻微抖动。根因是卡片向上移动后鼠标原本的位置不再位于卡片区域hover 状态被取消卡片又移回去反复触发。解决办法通常有两种给卡片包一层固定大小的容器hover 效果作用在容器上。在动画元素外围设置 padding 作为缓冲区域避免鼠标因位移脱离 hover 区域。从工程角度第一种更推荐因为它也顺便让布局更稳定。7. 最佳实践与工程建议7.1 用类名切换状态而不是直接写内联动画在项目中建议把动画和业务逻辑解耦。JS 只负责添加或删除表示状态的类名具体动画由 CSS 控制。这样状态可预测样式可复用也方便以后统一调整动画时长。// 推荐JS 只改类名 dialog.classList.add(dialog--open); // 不推荐JS 直接操作样式 // dialog.style.opacity 1; // dialog.style.transform translateY(0);7.2 使用 CSS 变量统一动画参数一个业务系统里动画时长、缓动曲线应该全局统一避免每个组件各自写一套值。可以抽成 CSS 变量:root { --duration-fast: 0.2s; --duration-normal: 0.3s; --duration-slow: 0.6s; --ease-standard: cubic-bezier(0.25, 0.1, 0.25, 1); --ease-in-out: cubic-bezier(0.42, 0, 0.58, 1); } .card { transition: transform var(--duration-normal) var(--ease-standard); }这样后续产品要求“动画再快点”只需要改变量不用满项目找散落的 transition。7.3 关注可访问性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; } }这种全局兜底写法适合大多数项目但要注意!important的权重问题。更好的做法是在具体组件里手动处理关键动画比如前面案例三那样在媒体查询里关闭进入动画并让内容直接可见。7.4 动画性能检查清单上线前如果页面包含较多动效建议按下面清单自查是否只动画了 transform 和 opacity是否有无限循环动画停留在页面后台切到后台标签页时应考虑暂停。是否给过多元素加了 will-change移动端是否避免使用较大面积的 box-shadow 动画是否在 DevTools Performance 面板实际录制过确认没有掉帧7.5 命名规范与维护动画类名建议带语义前缀便于检索。比如js-anim-表示用 JS 控制的动画类anim-表示纯 CSS 动画类。关键帧名称也要有意义不要叫aaa、test。/* 示例命名 */ keyframes fade-in-up { ... } keyframes spin { ... } keyframes pulse { ... }当项目动画数量变多建议按功能拆分文件比如animations/loading.css、animations/card.css避免一个样式文件膨胀到上千行。8. 总结与下一步到这里“做动画”从概念到实战的基本路径已经完整走了一遍transition 负责状态过渡transform 提供空间变换能力keyframes 和 animation 承担多阶段动画IntersectionObserver 帮助我们把动画和滚动位置关联起来性能优化则确保动画不会拖垮页面体验。如果接下来想继续深入可以按下面的顺序学习先用本文的三个案例练手改颜色、改时长、改缓动感受不同参数带来的视觉差异。学习 Web Animations API它能用 JavaScript 直接操作关键帧动画比手写 class 更灵活。尝试接入 GSAP 等动画库了解社区优秀方案如何组织复杂动画。如果需要做数据可视化、特殊图形动画可以学习 Canvas 和 SVG 动画。项目规模变大后重点研究动画与组件生命周期的关系特别是在弹窗、列表增删、路由切换这些场景中如何优雅处理动画。动画是前端体验的重要一环但它服务于内容和交互不要为了炫技而堆动效。实际开发中克制、流畅、一致比花哨更重要。把这套基础打牢以后无论接触什么动画库都能快速上手。