Angular CSS 原生动画实战指南:@keyframes、过渡与 enter/leave 动画
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
本文基于 Angular 官方文档站指南 Using CSS to animate your application,系统讲解在 Angular 应用中用纯 CSS(而非 Angular 动画包)实现完整动画体验的全部技术路径:从@keyframes定义可复用动画、类切换触发过渡、@starting-styles与animate.enter/animate.leave处理进出场,到交错(stagger)、并行组合、排序动画等复杂序列,以及通过 Web Animations API 对动画进行编程式控制。读完本文,你可以不依赖任何动画库,仅用原生 CSS + 少量 TypeScript 就覆盖日常开发中的绝大多数动画场景。
一、为什么选择原生 CSS 动画
CSS 提供了一套完整且强大的动画工具,可以直接在 Angular 应用组件样式中创建丰富、流畅的动效。如果你尚未写过原生 CSS 动画,可以先熟悉 MDN 的 CSS Animations 指南、W3Schools 的 CSS3 Animations 教程,以及 Net Ninja 的 CSS Animation 系列视频,然后再回到本指南。
Angular 官方文档站为此指南配套了一个可交互示例应用,所有示例组件都位于 adev/src/content/examples/animations/src/app/native-css 目录,每篇文章中的代码块均可在该目录找到原始文件。
二、创建可复用的 @keyframes 动画
通过@keyframes可以把动画定义放在一个共享 CSS 文件中,让应用各处复用同一组关键帧。官方示例定义了一个名为sharedAnimation的关键帧,并用.animated-class类将它绑定到元素上——只要把该 class 加到某个元素上,动画就会触发。
来自 animations.css:
@keyframes sharedAnimation { to { height: 0; opacity: 1; background-color: 'red'; } } .animated-class { animation: sharedAnimation 1s; }使用方式很简单:在模板中通过类绑定(或:host、@if等条件渲染)让元素获得animated-class类,即触发 1 秒的sharedAnimation关键帧动画。关键帧本身与具体组件解耦,是跨组件复用最自然的手段。
三、用 CSS 类动画化状态切换
很多场景需要动画化两个状态之间的变化,例如元素的“打开/关闭”。做法是:为每个状态定义一个 class,并在其中用transition描述属性如何从当前值平滑过渡到目标值。
来自 animations.css:
.open { height: '200px'; opacity: 1; background-color: 'yellow'; transition: all 1s; } .closed { height: '100px'; opacity: 0.8; background-color: 'blue'; transition: all 1s; }open/closed两个状态的触发,是在组件模板中切换元素上的 CSS 类。类绑定的具体写法([class.xxx]="expression")与直接动画化行内样式的写法,可参考 模板绑定指南 中的 CSS 类/样式属性绑定章节。
3.1 过渡、时长与缓动函数
调整时序(timing)、延迟(delay)与缓动(easing)几乎出现在每个动画中。CSS 提供了两种等价的方式:分别指定独立属性,或使用简写属性。
对关键帧动画,使用animation-duration、animation-delay、animation-timing-function,或animation简写;对不使用@keyframes的过渡动画,则使用transition-duration、transition-delay、transition-timing-function与transition简写。
来自 animations.css:
/* 关键帧动画的时序控制 */ .example-element { animation-duration: 1s; animation-delay: 500ms; animation-timing-function: ease-in-out; } .example-shorthand { animation: exampleAnimation 1s ease-in-out 500ms; } /* 过渡动画的时序控制 */ .example-element { transition-duration: 1s; transition-delay: 500ms; transition-timing-function: ease-in-out; transition-property: margin-right; } .example-shorthand { transition: margin-right 1s ease-in-out 500ms; }两个简写的取值顺序都是:名称/属性 时长 缓动 延迟。注意transition-property可以精确指定参与过渡的属性,避免transition: all带来的不可预期行为(下面的开/合示例即采用了transition-property的精确写法)。
3.2 完整示例:类切换触发开/合动画
动画的触发机制是:当某个类出现在元素上时,动画发生;移除该类后,元素回到该元素其他 CSS 规则定义的样子。下面这组官方示例完整展示了信号(signal)驱动的类切换 + CSS 过渡的组合。
open-close.ts:
import {Component, signal} from '@angular/core'; @Component({ selector: 'app-open-close', templateUrl: 'open-close.html', styleUrls: ['open-close.css'], }) export class OpenClose { isOpen = signal(true); toggle() { this.isOpen.update((isOpen) => !isOpen); } }open-close.html:
<h2>Open / Close Example</h2> <button type="button" class="toggle-btn" (click)="toggle()">Toggle Open/Close</button> <div class="open-close-container" [class.open]="isOpen()"> <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p> </div>open-close.css:
:host { display: block; margin-top: 1rem; } .open-close-container { border: 1px solid #dddddd; margin-top: 1em; padding: 20px 20px 0px 20px; font-weight: bold; font-size: 20px; height: 100px; opacity: 0.8; background: #3b82f6; color: #ebebeb; transition-property: height, opacity, background-color, color; transition-duration: 1s; } .toggle-btn { background: transparent; border: 1px solid var(--primary-contrast, black); color: var(--primary-contrast, black); padding: 10px 24px; border-radius: 8px; cursor: pointer; } .open { transition-duration: 0.5s; height: 200px; opacity: 1; background: #475569; color: #f9fafb; }要点解读:
- 默认样式(收起态)定义了
height: 100px与transition-property: height, opacity, background-color, color,因此任何向.open态或从.open态返回的属性变化都会走 1 秒过渡; .open类覆盖了目标状态值(height: 200px等),并把transition-duration单独调为0.5s,实现了“打开快、收起慢”的非对称节奏——这是纯 CSS 状态机的典型技巧:不同状态各自拥有不同的过渡参数;- 模板中
[class.open]="isOpen()"把布尔信号与类绑定,isOpen.update()完成状态翻转。
四、过渡与触发器
4.1 用 CSS Grid 动画化 auto 高度
“从固定高度过渡到内容自适应高度(auto)”是经典难题,因为height: auto无法直接插值。纯 CSS 的优雅解法是 CSS Grid 的fr行高技巧:把容器设为单列 Grid,在0fr与1fr之间过渡,子元素借助overflow: hidden裁切溢出,即可实现视觉上的高度自动展开/收起。
auto-height.ts 与 auto-height.html 沿用上文的信号 +[class.open]模式:
<button type="button" class="toggle-btn" (click)="toggle()">Toggle Open/Close</button> <div class="container" [class.open]="isOpen()"> <div class="content"> <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p> </div> </div>auto-height.css:
.container { display: grid; grid-template-rows: 0fr; overflow: hidden; transition: grid-template-rows 1s; } .container.open { grid-template-rows: 1fr; } .container .content { min-height: 0; transition: visibility 1s; padding: 0 20px; visibility: hidden; margin-top: 1em; font-weight: bold; font-size: 20px; background: #3b82f6; color: #ebebeb; overflow: hidden; } .container.open .content { visibility: visible; }关键细节:
grid-template-rows: 0fr → 1fr的过渡是核心——fr单位参与插值,0fr即“占 0 高度”,1fr即“占满内容所需高度”;- 子元素
.content必须overflow: hidden,且通过min-height: 0解除 Grid 子项的默认最小高度约束,否则折叠时内容会撑破容器; visibility同步用 1 秒过渡,让收起时内容淡出的时间点与高度变化一致。
如果你的浏览器支持范围足够新,还可以直接使用calc-size(),它是动画化 auto 尺寸的“真正解法”(可查阅 MDN 的calc-size()文档了解用法)。
4.2 元素进入视图的动画(animate.enter)
当元素因条件渲染“进入”DOM 时,可以用 Angular 的animate.enter指令:它在元素进入视图的那一刻应用指定的动画类。
insert.html:
<nav> <button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button> </nav> @if (isShown()) { <div class="insert-container" animate.enter="enter-animation"> <p>The box is inserted</p> </div> }insert.css:
.insert-container { border: 1px solid #dddddd; margin-top: 1em; padding: 20px; font-weight: bold; font-size: 20px; } .enter-animation { animation: slide-fade 1s; } @keyframes slide-fade { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }insert.ts 中的isShown = signal(false)配合@if控制插入/移除;当@if分支首次渲染、div进入 DOM 时,animate.enter添加enter-animation类,slide-fade关键帧播放一次(从下方 20px、透明滑入到原位)。
4.3 元素离开视图的动画(animate.leave)
离场动画与入场对称:用animate.leave指定元素离开视图时应用的 CSS 类。官方示例里,容器本身还用@starting-style定义了自己的入场初始值(透明),而离场则交给.deleting类的过渡完成。
remove.html:
@if (isShown()) { <div class="insert-container" animate.leave="deleting"> <p>The box is inserted</p> </div> }remove.css:
.insert-container { border: 1px solid #dddddd; margin-top: 1em; padding: 20px; font-weight: bold; font-size: 20px; opacity: 1; transition: opacity 200ms ease-in; @starting-style { opacity: 0; } } .deleting { opacity: 0; transform: translateY(20px); transition: opacity 500ms ease-out, transform 500ms ease-out; }工作流:元素插入时由@starting-style声明初始opacity: 0,随后过渡到opacity: 1(200ms 淡入);当isShown变为false、元素即将被移除时,animate.leave先加上deleting类,执行 500ms 的淡出 + 下移,动画结束后元素才被真正移出 DOM——这保证了“先播完离场动画再删除”的语义。
注意:子组件的
animate.leave动画只在同一组件模板内触发。嵌套组件的animate.leave动画不会因为父元素被移除而触发。更多细节见 Enter and Leave animations 指南。
4.4 数字增减动画(increment / decrement)
数字递增/递减是应用中的常见模式:数值变化时让文本快速放大变色,再回弹。这个示例展示了“手动加类 + 监听animationend事件移除类”的完整生命周期管理。
increment-decrement.ts:
import {Component, ElementRef, OnInit, signal, viewChild} from '@angular/core'; @Component({ selector: 'app-increment-decrement', templateUrl: 'increment-decrement.html', styleUrls: ['increment-decrement.css'], }) export class IncrementDecrement implements OnInit { num = signal(0); el = viewChild<ElementRef<HTMLParagraphElement>>('el'); ngOnInit() { this.el()?.nativeElement.addEventListener('animationend', (ev) => { if (ev.animationName.endsWith('decrement') || ev.animationName.endsWith('increment')) { this.animationFinished(); } }); } modify(n: number) { const targetClass = n > 0 ? 'increment' : 'decrement'; this.num.update((v) => (v += n)); this.el()?.nativeElement.classList.add(targetClass); } animationFinished() { this.el()?.nativeElement.classList.remove('increment', 'decrement'); } ngOnDestroy() { this.el()?.nativeElement.removeEventListener('animationend', this.animationFinished); } }increment-decrement.html:
<h3>Increment and Decrement Example</h3> <section> <p #el>Number {{ num() }}</p> <div class="controls"> <button type="button" (click)="modify(1)">+</button> <button type="button" (click)="modify(-1)">-</button> </div> </section>increment-decrement.css 中定义了两组关键帧:
.increment { animation: increment 300ms; } .decrement { animation: decrement 300ms; } @keyframes increment { 33% { color: green; transform: scale(1.3, 1.2); } 66% { color: green; transform: scale(1.2, 1.2); } 100% { transform: scale(1, 1); } } @keyframes decrement { 33% { color: red; transform: scale(0.8, 0.9); } 66% { color: red; transform: scale(0.9, 0.9); } 100% { transform: scale(1, 1); } }这条示例完整体现了纯 CSS 动画在 Angular 中的典型工程模式:
- 模板引用(
#el+viewChild)拿到 DOM 元素句柄; - 状态变化时手动
classList.add('increment' | 'decrement'); - 监听
animationend事件,按animationName校验后移除类——类移除后关键帧动画即结束,为下一次触发做好准备; - 关键帧用 33% / 66% / 100% 三段做“放大—回落”的弹性节奏。
4.5 禁用动画的三种方式
当需要让某些动画不播放时,文档给出了三条路径:
方式一:强制置空的自定义类。定义一个把animation和transition都设为none的类,应用到元素上即可阻止该元素上任何动画:
.no-animation { animation: none !important; transition: none !important; }你可以把它作用在单个元素上,也可以作用到整个 DOM 或 DOM 的某个区段以全局强制。但要注意:这会连带阻止动画事件的触发。如果你的逻辑依赖等待动画事件来完成元素移除(例如离场动画结束后才删除节点),这种方案会失效——文档给出的替代做法是把动画时长设为 1 毫秒,既接近瞬时,又保留事件语义。
方式二:prefers-reduced-motion媒体查询。利用系统级的prefers-reduced-motion媒体查询(MDN 有专页说明该查询),在用户系统偏好“减少动态效果”时自动关闭所有动画,这是无障碍(a11y)层面的标准做法。
方式三:程序化地阻止添加动画类。在组件逻辑里直接控制:不添加触发动画的类,动画自然不播放。这是最可控、也最容易与业务状态联动的方式。
4.6 动画回调事件
如果需要在动画生命周期的特定时点执行逻辑,DOM 提供了完整的事件监听面:
- 关键帧动画事件:
animationstart、animationend、animationiteration(每次循环迭代)、animationcancel; - 过渡事件:
transitionstart、transitionrun、transitionend、transitioncancel。
Web Animations API 还暴露了远超 CSS 声明式能力的编程接口(可查阅 MDN 的 Web Animations API 文档了解全貌)。
注意事件冒泡(bubbling):当父、子元素都在做动画时,子元素的事件会冒泡到父元素上。此时应考虑
stopPropagation(),或检查事件对象中的animationName、正在过渡的属性等信息,确认自己响应的是目标节点的事件,而不是从子节点冒泡上来的事件。
五、复杂动画序列
真实应用的动画往往不止一个淡入淡出,而是多个动画组合编排。
5.1 列表项交错(stagger)动画
让列表中每一项依次延迟入场,形成级联效果,核心是animation-delay/transition-delay配合每项的索引。官方示例把索引写入 CSS 自定义属性--index,用calc()计算逐项延迟:
stagger.html:
<h1>Stagger Example</1> <button type="button" class="toggle-btn" (click)="refresh()">Refresh</button> <div class="items-container"> @if (show()) { <ul class="items"> @for (item of items; track $index) { <li class="item" style="--index: {{ $index }}">{{ item }}</li> } </ul> } </div>stagger.css:
.items .item { transition-property: opacity, transform; transition-duration: 500ms; transition-delay: calc(200ms * var(--index)); @starting-style { opacity: 0; transform: translateX(-10px); } }stagger.ts 的refresh()先把show置为false,10ms 后再置回true,通过“移除再插入”重新触发@starting-style驱动的入场过渡。
拆解要点:
- 每个
<li>通过内联样式携带--index(0、1、2……),transition-delay: calc(200ms * var(--index))使第 n 项晚 200ms×n 开始过渡,天然形成瀑布节奏; @starting-style声明了元素首次出现在文档中时的起始样式(透明 + 左移 10px),之后过渡到默认样式,实现无需@keyframes的入场动画;- 外层
@if (show())的整体插拔是重播手段——这也说明@for项在“移除并重新加入”时会重新触发入场动画(见下文排序示例)。
5.2 并行动画(Parallel Animations)
animation简写支持在一条声明里列出多个动画,每个动画可拥有独立的时长与延迟,从而在一个元素上并行组合多个效果:
.target-element { animation: rotate 3s, fade-in 2s; }在这个例子中,rotate(3s)与fade-in(2s)同时开始、同时结束各自的生命周期,互不干扰。这是纯 CSS 下“组合出复杂效果”最轻量的手段。
5.3 列表重排序动画(reorder)
@for循环中的项目在数据顺序变化时会被移除并重新加入,这会触发入场动画——配合@starting-styles(即@starting-style块)即可获得“换位/重排时重新淡入”的效果;也可以改用animate.enter达到同样的行为。对于被移除的项目,用animate.leave播放离场动画:
reorder.html:
<h1>Reordering List Example</1> <button type="button" class="toggle-btn" (click)="randomize()">Randomize</button> <ul class="items"> @for (item of items; track item) { <li class="item" animate.leave="fade">{{ item }}</li> } </ul>reorder.css:
.items .item { transition-property: opacity, transform; transition-duration: 500ms; @starting-style { opacity: 0; transform: translateX(-10px); } } .items .item.fade { animation: fade-out 500ms; } @keyframes fade-out { from { opacity: 1; } to { opacity: 0; } }reorder.ts 的randomize()通过 Fisher-Yates 风格的洗牌重排items数组;模板以track item追踪项身份,身份不变的项保持 DOM 节点,仅顺序改变,而被判定移除的项在离开前由animate.leave="fade"播放fade-out关键帧。这样重排既有序变化,又有离场/重入的视觉反馈。
六、动画的编程式控制
除了声明式地“加类即播放”,还可以直接从元素上取回正在运行的动画对象:调用 DOM 的Element.getAnimations(),它会返回该元素上所有Animation实例的数组。拿到Animation对象后,Web Animations API 提供的能力远超旧版动画包的AnimationPlayer——你可以对每个动画cancel()、play()、pause()、reverse(),乃至操作currentTime、playbackRate等属性。对绝大多数“控制动画”的需求,这套原生 API 已经足够。
七、延伸阅读
如果你想继续深入 Angular 的动画体系,可以阅读文档站中的以下指南:
- Enter and Leave animations(进入与离开动画) ——
animate.enter/animate.leave的完整语义与边界; - Route transition animations(路由过渡动画) —— 路由切换场景的动画方案;
- 动画指南总入口:guide/animations。
小结
| 场景 | 核心技术 | 参考实现 |
|---|---|---|
| 跨组件复用 | @keyframes+ 共享 CSS 类 | animations.css |
| 状态切换 | [class.x]+transition | open-close.css |
| auto 高度 | Grid0fr → 1fr过渡 | auto-height.css |
| 入场/离场 | animate.enter/animate.leave+@starting-style | insert.css、remove.css |
| 数字增减 | 手动加类 +animationend移除类 | increment-decrement.ts |
| 级联/交错 | transition-delay: calc(200ms * var(--index)) | stagger.css |
| 列表重排 | @for移除重插 +@starting-style+animate.leave | reorder.ts |
| 编程控制 | Element.getAnimations()+ Web Animations API | DOM 标准 API |
| 关闭动画 | .no-animation类 /prefers-reduced-motion/ 不添加类 | 见“禁用动画”一节 |
纯 CSS 方案的优势在于零依赖、性能好(合成层属性transform/opacity可走 GPU 加速)、且与 Angular 信号驱动的响应式模型天然契合:组件只负责翻转信号与类,CSS 负责一切视觉插值。掌握本文的类切换、@starting-style、animate.enter/animate.leave与事件回调四条主线,即可覆盖 Angular 应用中绝大多数动画需求。
【免费下载链接】angularDeliver web apps with confidence 🚀项目地址: https://gitcode.com/GitHub_Trending/an/angular
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考