v20.2以降、@angular/animationsパッケージは非推奨になり、同時にアプリケーションへアニメーションを追加するための新しいanimate.enterおよびanimate.leave機能が導入されました。これらの新機能を使うと、@angular/animationsベースのアニメーションをすべて、プレーンなCSSまたはJSアニメーションライブラリで置き換えられます。アプリケーションから@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クラスを使うことで、ネイティブにも実現できます。
.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プロパティまたはショートハンドプロパティを使って実現できます。
キーフレームアニメーションでは、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へのアニメーションはopen => closedとなります。ワイルドカードを使うと任意の状態から対象状態へのマッチングができ、* => closedのように書けます。また、voidキーワードはenterおよびexitの状態に使えます。たとえば、要素がビューを離れるときは* => void、要素がビューに入るときはvoid => *です。
CSSで直接アニメーション化する場合、こうした状態マッチングパターンはまったく不要です。要素に設定するクラスやスタイルに応じて、どのトランジションや@keyframesアニメーションを適用するかを管理できます。DOMに入った直後の要素の見た目を制御するために@starting-styleを追加できます。
アニメーションパッケージでは、固定した高さから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を使うと、height: autoへのアニメーションを実現できます。
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;
}
すべてのブラウザをサポートする必要がない場合は、height: autoをアニメーション化するための本命の解決策であるcalc-size()も確認してください。詳しくは、MDNのドキュメントとこのチュートリアルを参照してください。
アニメーションパッケージでは、前述のenterとleaveのパターンマッチングに加えて、: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のアニメーションガイドを参照してください。
前述の: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);
}
}
アニメーションパッケージとは異なり、あるコンポーネント内に複数のアニメーションを指定しても、どのアニメーションも他より優先されず、どのアニメーションの発火もブロックされません。アニメーションの順序付けは、animationやtransitionの遅延を使ったCSSアニメーション定義、あるいは次にアニメーションさせるCSSを追加するためのanimationendまたはtransitionendによって処理する必要があります。
Child animations fire only within the same component template. In the @angular/animations package, parent animations could query and trigger animations in nested child components using query() and animateChild(). With animate.leave and native CSS animations, animations defined inside nested child component templates will not fire when a parent component removes an element or view. Only nested animations within the same Angular component template will execute. See the Enter and Leave Animations guide for more information on this.
ネイティブCSSアニメーションでは、指定したアニメーションを無効にしたい場合、複数の選択肢があります。
- animationとtransitionを
noneに強制するカスタムクラスを作成します。
.no-animation {
animation: none !important;
transition: none !important;
}
このクラスを要素に適用すると、その要素ではどのアニメーションも発火しなくなります。代わりにDOM全体またはDOMの一部にスコープして、この挙動を強制できます。ただし、この方法ではアニメーションイベントが発火しなくなります。要素削除のためにアニメーションイベントを待っている場合、この方法は機能しません。回避策としては、継続時間を1ミリ秒に設定します。
prefers-reduced-motionメディアクエリを使用して、アニメーションを控えたいユーザーにはアニメーションを再生しないようにします。
プログラムからアニメーションクラスを追加しないようにします。
アニメーションパッケージは、アニメーション終了時に何かをしたい場合に使用できるコールバックを公開していました。ネイティブCSSアニメーションにも同様のコールバックがあります。
OnAnimationStart
OnAnimationEnd
OnAnimationIteration
OnAnimationCancel
OnTransitionStart
OnTransitionRun
OnTransitionEnd
OnTransitionCancel
Web Animations APIには、他にも多くの機能があります。利用できるすべてのアニメーションAPIについては、ドキュメントを参照してください。
NOTE: これらのコールバックではバブリングの問題に注意してください。子要素と親要素の両方をアニメーション化している場合、イベントは子要素から親要素へバブルアップします。子ノードからバブルしてきたイベントではなく、意図したイベントターゲットに応答しているかを判断するために、伝播を停止するか、イベントの詳細を確認してください。適切なノードを対象にしているかは、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ループ内の項目は適切に削除されて再追加されるため、enterアニメーションとして@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クラスを使うと、コードからアニメーションを一時停止、再生、再開、完了するなど、より高度な操作をするためにアニメーションへアクセスできます。これらはすべてネイティブでも処理できます。
Element.getAnimations()を使うと、要素に関連付けられたアニメーションを直接取得できます。これは、その要素上のすべてのAnimationを配列として返します。Animation APIを使えば、アニメーションパッケージのAnimationPlayerが提供していたものよりも多くのことを行えます。ここからcancel()、play()、pause()、reverse()などを呼び出せます。このネイティブAPIだけで、アニメーションを制御するために必要な機能が揃うはずです。
ルート間をアニメーションさせるには、ビュー遷移を使用できます。始めるには、ルートトランジションアニメーションガイドを参照してください。