@angular/animations 패키지는 v20.2부터 사용 중단 권고(deprecated) 상태이며, 같은 버전에 애플리케이션에 애니메이션을 추가하는 새로운 기능인 animate.enter와 animate.leave가 도입되었습니다.
이 기능을 사용하면 @angular/animations 기반 애니메이션을 모두 일반 CSS나 JavaScript 애니메이션 라이브러리로 대체할 수 있습니다.
애플리케이션에서 @angular/animations를 제거하면 JavaScript 번들 크기를 크게 줄일 수 있습니다.
네이티브 CSS 애니메이션은 하드웨어 가속을 활용할 수 있어 일반적으로 더 뛰어난 성능을 제공합니다.
이 가이드에서는 @angular/animations를 사용하는 코드를 네이티브 CSS 애니메이션으로 리팩토링하는 과정을 살펴봅니다.
네이티브 CSS 애니메이션을 작성해 본 적이 없다면, 시작하는 데 도움이 되는 훌륭한 가이드가 많이 있습니다.
그중 몇 가지를 소개합니다:
MDN CSS 애니메이션 가이드
W3Schools CSS3 애니메이션 가이드
CSS 애니메이션 전체 튜토리얼
초보자를 위한 CSS 애니메이션
다음 동영상도 참고할 수 있습니다:
9분 만에 배우는 CSS 애니메이션
Net Ninja CSS 애니메이션 튜토리얼 재생목록
이 가이드와 튜토리얼 중 몇 가지를 살펴본 뒤 이 문서로 돌아오세요.
애니메이션 패키지를 사용할 때처럼 애플리케이션 전체에서 공유할 수 있는 재사용 가능한 애니메이션을 만들 수 있습니다.
애니메이션 패키지에서는 공통 TypeScript 파일에서 animation() 함수를 사용했습니다.
네이티브 CSS 방식도 비슷하지만, 공통 CSS 파일에 정의합니다.
export const sharedAnimation = animation([
style({
height: 0,
opacity: 1,
backgroundColor: 'red',
}),
animate('1s'),
]);
@keyframes sharedAnimation {
to {
height: 0;
opacity: 1;
background-color: 'red';
}
}
.animated-class {
animation: sharedAnimation 1s;
}
엘리먼트에 animated-class 클래스를 추가하면 해당 엘리먼트에서 애니메이션이 실행됩니다.
애니메이션 패키지에서는 컴포넌트 안에서 state() 함수로 여러 상태를 정의할 수 있었습니다.
예를 들어 open이나 closed 상태를 정의하면서 각 상태에 해당하는 스타일을 포함할 수 있습니다.
다음 예제를 참고하세요:
// ...
state(
'open',
style({
height: '200px',
opacity: 1,
backgroundColor: 'yellow',
}),
),
CSS 클래스에 키프레임 애니메이션이나 전환 스타일을 지정하면 네이티브 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 상태가 실행됩니다.
방법은 템플릿 가이드의 예제를 참고하세요.
템플릿 가이드의 스타일에 직접 애니메이션 적용하기에서도 비슷한 예제를 확인할 수 있습니다.
애니메이션 패키지의 animate() 함수에서는 지속 시간, 지연 시간, 이징 같은 타이밍을 지정할 수 있습니다.
CSS의 여러 프로퍼티나 단축 프로퍼티를 사용하면 네이티브 CSS로도 설정할 수 있습니다.
CSS 키프레임 애니메이션에는 animation-duration, animation-delay, animation-timing-function을 지정하거나, 단축 프로퍼티인 animation을 사용하세요.
.example-element {
animation-duration: 1s;
animation-delay: 500ms;
animation-timing-function: ease-in-out;
}
.example-shorthand {
animation: exampleAnimation 1s ease-in-out 500ms;
}
마찬가지로 @keyframes를 사용하지 않는 애니메이션에는 transition-duration, transition-delay, transition-timing-function과 단축 프로퍼티 transition을 사용할 수 있습니다.
.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;
}
애니메이션 패키지에서는 trigger() 함수로 트리거를 지정하고, 그 안에 모든 상태를 중첩해서 정의해야 했습니다.
네이티브 CSS에서는 그럴 필요가 없습니다.
CSS 스타일이나 클래스를 전환해서 애니메이션을 실행할 수 있습니다.
엘리먼트에 클래스가 추가되면 애니메이션이 실행됩니다.
클래스를 제거하면 엘리먼트는 원래 정의된 CSS 스타일로 돌아갑니다.
따라서 훨씬 적은 코드로 같은 애니메이션을 구현할 수 있습니다.
다음 예제를 참고하세요:
import {Component, signal} from '@angular/core';
import {trigger, transition, state, animate, style, keyframes} from '@angular/animations';
@Component({
selector: 'app-open-close',
animations: [
trigger('openClose', [
state(
'open',
style({
height: '200px',
opacity: 1,
backgroundColor: 'yellow',
}),
),
state(
'closed',
style({
height: '100px',
opacity: 0.5,
backgroundColor: 'green',
}),
),
// ...
transition('* => *', [
animate(
'1s',
keyframes([
style({opacity: 0.1, offset: 0.1}),
style({opacity: 0.6, offset: 0.2}),
style({opacity: 1, offset: 0.5}),
style({opacity: 0.2, offset: 0.7}),
]),
),
]),
]),
],
templateUrl: 'open-close.html',
styleUrl: 'open-close.css',
})
export class OpenClose {
isOpen = signal(false);
toggle() {
this.isOpen.update((isOpen) => !isOpen);
}
}
<nav>
<button type="button" (click)="toggle()">Toggle Open/Close</button>
</nav>
<div [@openClose]="isOpen() ? 'open' : 'closed'" class="open-close-container">
<p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
</div>
:host {
display: block;
margin-top: 1rem;
}
.open-close-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px 20px 0px 20px;
color: #000000;
font-weight: bold;
font-size: 20px;
}
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);
}
}
<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>
: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;
}
애니메이션 패키지에서는 문자열을 사용해서 정의한 상태를 전환과 매칭할 수 있습니다.
예를 들어 열린 상태에서 닫힌 상태로 전환하는 애니메이션은 open => closed로 표현합니다.
* => closed처럼 와일드카드를 사용하면 모든 상태를 목표 상태와 매칭할 수 있고, void 키워드로 진입과 퇴장 상태를 표현할 수도 있습니다.
예를 들어 엘리먼트가 뷰에서 퇴장할 때는 * => void, 뷰에 진입할 때는 void => *를 사용합니다.
CSS로 직접 애니메이션을 적용할 때는 이런 상태 매칭 패턴이 전혀 필요하지 않습니다.
엘리먼트에 지정한 클래스나 스타일을 기준으로 어떤 전환과 @keyframes 애니메이션을 적용할지 관리하면 됩니다.
@starting-style을 추가해서 엘리먼트가 DOM에 진입하는 순간의 모습을 제어할 수도 있습니다.
애니메이션 패키지는 고정된 높이에서 height: auto로 바꾸는 것처럼, 기존에는 구현하기 어려웠던 애니메이션을 지원합니다.
이제 순수 CSS로도 구현할 수 있습니다.
import {Component, signal} from '@angular/core';
import {trigger, transition, state, animate, style} from '@angular/animations';
@Component({
selector: 'app-open-close',
animations: [
trigger('openClose', [
state('true', style({height: '*'})),
state('false', style({height: '0px'})),
transition('false <=> true', animate(1000)),
]),
],
templateUrl: 'auto-height.html',
styleUrl: 'auto-height.css',
})
export class AutoHeight {
isOpen = signal(false);
toggle() {
this.isOpen.update((isOpen) => !isOpen);
}
}
<h2>Auto Height Example</h2>
<button type="button" (click)="toggle()">Toggle Open/Close</button>
<div class="container" [@openClose]="isOpen() ? true : false">
<div class="content">
<p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p>
</div>
</div>
.container {
display: block;
overflow: hidden;
}
.container .content {
padding: 20px;
margin-top: 1em;
font-weight: bold;
font-size: 20px;
background: #3b82f6;
color: #ebebeb;
}
CSS Grid를 사용하면 높이가 자동으로 결정되는 상태까지 애니메이션을 적용할 수 있습니다.
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-auto-height',
templateUrl: 'auto-height.html',
styleUrls: ['auto-height.css'],
})
export class AutoHeight {
isOpen = signal(true);
toggle() {
this.isOpen.update((isOpen) => !isOpen);
}
}
<h2>Auto Height Example</h2>
<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>
.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;
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
모든 브라우저를 지원할 필요가 없다면, 자동 높이 애니메이션을 직접 지원하는 calc-size()도 살펴보세요.
자세한 내용은 MDN 문서와 (이 튜토리얼)[https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/]을 참고하세요.
애니메이션 패키지는 앞서 설명한 진입 및 퇴장 패턴 매칭뿐 아니라 :enter, :leave라는 축약 별칭도 제공했습니다.
import {Component} from '@angular/core';
import {trigger, transition, animate, style} from '@angular/animations';
@Component({
selector: 'app-insert-remove',
animations: [
trigger('myInsertRemoveTrigger', [
transition(':enter', [style({opacity: 0}), animate('200ms', style({opacity: 1}))]),
transition(':leave', [animate('200ms', style({opacity: 0}))]),
]),
],
templateUrl: 'insert-remove.html',
styleUrls: ['insert-remove.css'],
})
export class InsertRemove {
isShown = false;
toggle() {
this.isShown = !this.isShown;
}
}
<h2>Insert/Remove</h2>
<nav>
<button type="button" (click)="toggle()">Toggle Insert/Remove</button>
</nav>
@if (isShown) {
<div @myInsertRemoveTrigger class="insert-remove-container">
<p>The box is inserted</p>
</div>
}
:host {
display: block;
}
.insert-remove-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px 20px 0px 20px;
color: #000000;
font-weight: bold;
font-size: 20px;
}
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-insert',
templateUrl: 'insert.html',
styleUrls: ['insert.css'],
})
export class Insert {
isShown = signal(false);
toggle() {
this.isShown.update((isShown) => !isShown);
}
}
<h2>Insert Element Example</h2>
<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>
}
:host {
display: block;
}
.insert-container {
border: 1px solid #dddddd;
margin-top: 1em;
padding: 20px;
font-weight: bold;
font-size: 20px;
}
.insert-container p {
margin: 0;
}
.enter-animation {
animation: slide-fade 1s;
}
@keyframes slide-fade {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-remove',
templateUrl: 'remove.html',
styleUrls: ['remove.css'],
})
export class Remove {
isShown = signal(false);
toggle() {
this.isShown.update((isShown) => !isShown);
}
}
<h2>Remove Element Example</h2>
<nav>
<button type="button" class="toggle-btn" (click)="toggle()">Toggle Element</button>
</nav>
@if (isShown()) {
<div class="insert-container" animate.leave="deleting">
<p>The box is inserted</p>
</div>
}
:host {
display: block;
}
.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;
}
}
.insert-container p {
margin: 0;
}
.deleting {
opacity: 0;
transform: translateY(20px);
transition:
opacity 500ms ease-out,
transform 500ms ease-out;
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
animate.enter와 animate.leave에 대한 자세한 내용은 진입 및 퇴장 애니메이션 가이드를 참고하세요.
앞서 설명한 :enter, :leave 외에도 :increment와 :decrement가 있습니다.
클래스를 추가하고 제거하는 방식으로 이 애니메이션도 구현할 수 있습니다.
애니메이션 패키지의 내장 별칭과는 달리, 값이 증가하거나 감소할 때 클래스가 자동으로 적용되지는 않습니다.
코드에서 적절한 클래스를 적용하면 됩니다.
다음 예제를 참고하세요:
import {Component, signal} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';
@Component({
selector: 'app-increment-decrement',
templateUrl: 'increment-decrement.html',
styleUrls: ['increment-decrement.css'],
animations: [
trigger('incrementAnimation', [
transition(':increment', [
animate('300ms ease-out', style({color: 'green', transform: 'scale(1.3, 1.2)'})),
]),
transition(':decrement', [
animate('300ms ease-out', style({color: 'red', transform: 'scale(0.8, 0.9)'})),
]),
]),
],
})
export class IncrementDecrement {
num = signal(0);
modify(n: number) {
this.num.update((v) => (v += n));
}
}
<h3>Increment and Decrement Example</h3>
<section>
<p [@incrementAnimation]="num()">Number {{ num() }}</p>
<div class="controls">
<button type="button" (click)="modify(1)">+</button>
<button type="button" (click)="modify(-1)">-</button>
</div>
</section>
:host {
display: block;
font-size: 32px;
margin: 20px;
text-align: center;
}
section {
border: 1px solid lightgray;
border-radius: 50px;
}
p {
display: inline-block;
margin: 2rem 0;
text-transform: uppercase;
}
.controls {
padding-bottom: 2rem;
}
button {
font: inherit;
border: 0;
background: lightgray;
width: 50px;
border-radius: 10px;
}
button + button {
margin-left: 10px;
}
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);
}
}
<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>
:host {
display: block;
font-size: 32px;
margin: 20px;
text-align: center;
}
section {
border: 1px solid lightgray;
border-radius: 50px;
}
p {
display: inline-block;
margin: 2rem 0;
text-transform: uppercase;
}
.increment {
animation: increment 300ms;
}
.decrement {
animation: decrement 300ms;
}
.controls {
padding-bottom: 2rem;
}
button {
font: inherit;
border: 0;
background: lightgray;
width: 50px;
border-radius: 10px;
}
button + button {
margin-left: 10px;
}
@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 애니메이션 정의에서 애니메이션이나 전환의 지연 시간을 지정하거나, animationend 또는 transitionend 이벤트에서 다음 애니메이션을 위한 CSS를 추가하도록 처리해야 합니다.
자식 애니메이션은 같은 컴포넌트 템플릿 안에서만 실행됩니다. @angular/animations 패키지에서는 부모 애니메이션이 query()와 animateChild()로 중첩된 자식 컴포넌트의 애니메이션을 찾아 실행할 수 있었습니다. animate.leave와 네이티브 CSS 애니메이션에서는 부모 컴포넌트가 엘리먼트나 뷰를 제거해도 중첩된 자식 컴포넌트 템플릿 안의 애니메이션은 실행되지 않습니다. 같은 Angular 컴포넌트 템플릿 안에 중첩된 애니메이션만 실행됩니다. 자세한 내용은 진입/퇴장 애니메이션 가이드를 참고하세요.
네이티브 CSS 애니메이션에서는 지정한 애니메이션을 비활성화하려면 몇 가지 방법을 사용할 수 있습니다.
- 애니메이션과 전환을
none으로 강제 설정하는 커스텀 클래스를 만듭니다.
.no-animation {
animation: none !important;
transition: none !important;
}
이 클래스를 엘리먼트에 적용하면 해당 엘리먼트에서 애니메이션이 실행되지 않습니다.
DOM 전체나 특정 영역에 적용해서 같은 동작을 강제할 수도 있습니다.
하지만 이 방법은 애니메이션 이벤트도 발생하지 않게 합니다.
엘리먼트를 제거하기 위해 애니메이션 이벤트를 기다리고 있다면 이 방법은 사용할 수 없습니다.
대신 지속 시간을 1밀리초로 설정하는 방법이 있습니다.
prefers-reduced-motion 미디어 쿼리를 사용해서 움직임을 줄이도록 설정한 사용자에게는 애니메이션을 실행하지 않습니다.
코드에서 애니메이션 클래스가 추가되지 않도록 처리합니다.
애니메이션 패키지는 애니메이션이 끝났을 때 어떤 동작을 실행할 수 있도록 콜백을 제공했습니다.
네이티브 CSS 애니메이션에도 이런 콜백이 있습니다.
OnAnimationStart
OnAnimationEnd
OnAnimationIteration
OnAnimationCancel
OnTransitionStart
OnTransitionRun
OnTransitionEnd
OnTransitionCancel
Web Animations API는 이 밖에도 많은 기능을 제공합니다.
사용할 수 있는 애니메이션 API 전체는 문서를 참고하세요.
참고: 이런 콜백에서는 버블링에 주의하세요.
자식과 부모에 애니메이션을 적용하면 이벤트가 자식에서 부모로 버블링됩니다.
이벤트 전파를 중단하거나 이벤트의 상세 정보를 확인해서, 자식 노드에서 버블링된 이벤트가 아니라 의도한 대상의 이벤트에 반응하는지 판단하세요.
animationname 프로퍼티나 전환 중인 프로퍼티를 조사하면 올바른 노드인지 확인할 수 있습니다.
애니메이션 패키지는 복잡한 시퀀스를 만드는 기능을 기본으로 제공합니다.
이 시퀀스는 모두 애니메이션 패키지 없이도 구현할 수 있습니다.
애니메이션 패키지에서는 document.querySelector()와 비슷하게 query() 함수로 CSS 클래스 이름에 해당하는 특정 엘리먼트를 찾을 수 있었습니다.
네이티브 CSS 애니메이션에서는 이 함수가 필요하지 않습니다.
대신 CSS 셀렉터로 하위 엘리먼트의 클래스를 선택하고 원하는 transform이나 animation을 적용할 수 있습니다.
템플릿의 자식 노드에서 클래스를 전환하려면 클래스 바인딩과 스타일 바인딩을 사용해서 적절한 시점에 애니메이션을 추가하면 됩니다.
stagger() 함수는 목록의 각 항목에 지정된 시간만큼 지연을 두고 애니메이션을 실행해서 연속적으로 퍼지는 효과를 만들 수 있게 했습니다.
네이티브 CSS에서는 animation-delay나 transition-delay로 같은 동작을 구현할 수 있습니다.
다음 예제에서 CSS를 어떻게 작성하는지 확인할 수 있습니다.
import {Component, HostBinding, signal} from '@angular/core';
import {trigger, transition, animate, style, query, stagger} from '@angular/animations';
@Component({
selector: 'app-stagger',
templateUrl: 'stagger.html',
styleUrls: ['stagger.css'],
animations: [
trigger('pageAnimations', [
transition(':enter', [
query('.item', [
style({opacity: 0, transform: 'translateY(-10px)'}),
stagger(200, [animate('500ms ease-in', style({opacity: 1, transform: 'none'}))]),
]),
]),
]),
],
})
export class Stagger {
@HostBinding('@pageAnimations')
items = [1, 2, 3];
}
<h2>Stagger Example</h2>
<ul class="items">
@for (item of items; track item) {
<li class="item">{{ item }}</li>
}
</ul>
.items {
list-style: none;
padding: 0;
margin: 0;
}
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-stagger',
templateUrl: './stagger.html',
styleUrls: ['stagger.css'],
})
export class Stagger {
show = signal(true);
items = [1, 2, 3];
refresh() {
this.show.set(false);
setTimeout(() => {
this.show.set(true);
}, 10);
}
}
<h1>Stagger Example</h1>
<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>
.items-container {
min-height: 4.5rem;
}
.items {
list-style: none;
padding: 0;
margin: 0;
}
.items .item {
transition-property: opacity, transform;
transition-duration: 500ms;
transition-delay: calc(200ms * var(--index));
@starting-style {
opacity: 0;
transform: translateX(-10px);
}
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
애니메이션 패키지에는 여러 애니메이션을 동시에 실행하는 group() 함수가 있습니다.
CSS에서는 애니메이션의 타이밍을 자유롭게 제어할 수 있습니다.
여러 애니메이션을 정의했다면 모두 한 번에 적용할 수 있습니다.
.target-element {
animation:
rotate 3s,
fade-in 2s;
}
이 예제에서는 rotate와 fade-in 애니메이션이 동시에 시작합니다.
앞서 설명한 기법을 사용하면 목록 항목의 순서가 바뀌는 경우에도 별도의 추가 작업 없이 바로 동작합니다.
@for 반복문의 항목은 올바르게 제거된 뒤 다시 추가되며, 이때 @starting-styles를 사용하는 진입 애니메이션이 실행됩니다.
같은 동작을 구현하기 위해 animate.enter를 사용할 수도 있습니다.
위 예제처럼 엘리먼트가 제거될 때 애니메이션을 적용하려면 animate.leave를 사용하세요.
import {Component, signal} from '@angular/core';
import {trigger, transition, animate, query, style} from '@angular/animations';
@Component({
selector: 'app-reorder',
templateUrl: './reorder.html',
styleUrls: ['reorder.css'],
animations: [
trigger('itemAnimation', [
transition(':enter', [
style({opacity: 0, transform: 'translateX(-10px)'}),
animate('300ms', style({opacity: 1, transform: 'translateX(none)'})),
]),
transition(':leave', [
style({opacity: 1, transform: 'translateX(none)'}),
animate('300ms', style({opacity: 0, transform: 'translateX(-10px)'})),
]),
]),
],
})
export class Reorder {
show = signal(true);
items = ['stuff', 'things', 'cheese', 'paper', 'scissors', 'rock'];
randomize() {
const randItems = [...this.items];
const newItems = [];
for (let i of this.items) {
const max: number = this.items.length - newItems.length;
const randNum = Math.floor(Math.random() * max);
newItems.push(...randItems.splice(randNum, 1));
}
this.items = newItems;
}
}
<h1>Reordering List Example</h1>
<button type="button" (click)="randomize()">Randomize</button>
<ul class="items">
@for (item of items; track item) {
<li @itemAnimation class="item">{{ item }}</li>
}
</ul>
.items {
list-style: none;
padding: 0;
margin: 0;
}
import {Component, signal} from '@angular/core';
@Component({
selector: 'app-reorder',
templateUrl: './reorder.html',
styleUrls: ['reorder.css'],
})
export class Reorder {
show = signal(true);
items = ['stuff', 'things', 'cheese', 'paper', 'scissors', 'rock'];
randomize() {
const randItems = [...this.items];
const newItems = [];
for (let i of this.items) {
const max: number = this.items.length - newItems.length;
const randNum = Math.floor(Math.random() * max);
newItems.push(...randItems.splice(randNum, 1));
}
this.items = newItems;
}
}
<h1>Reordering List Example</h1>
<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>
.items {
list-style: none;
padding: 0;
margin: 0;
}
.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;
}
}
.toggle-btn {
background: transparent;
border: 1px solid var(--primary-contrast, black);
color: var(--primary-contrast, black);
padding: 10px 24px;
border-radius: 8px;
cursor: pointer;
}
AnimationPlayer 클래스를 사용하면 애니메이션에 접근해서 코드로 일시 중지, 재생, 다시 시작, 종료 같은 고급 작업을 할 수 있습니다.
이 작업은 모두 네이티브 API로도 처리할 수 있습니다.
Element.getAnimations()로 엘리먼트의 애니메이션을 직접 가져올 수 있습니다.
이 메서드는 해당 엘리먼트의 모든 Animation을 배열로 반환합니다.
Animation API를 사용하면 애니메이션 패키지의 AnimationPlayer가 제공했던 것보다 훨씬 많은 작업을 할 수 있습니다.
cancel(), play(), pause(), reverse() 등 다양한 메서드를 사용할 수 있습니다.
이 네이티브 API는 애니메이션을 제어하는 데 필요한 모든 기능을 제공합니다.
뷰 전환을 사용해서 라우팅 규칙 사이에 애니메이션을 적용할 수 있습니다.
시작하려면 라우팅 규칙 전환 애니메이션 가이드를 참고하세요.