Animasi Angular mengatur perpindahan sebuah elemen dari satu tampilan ke tampilan lain: baris keranjang yang meluncur masuk dan memudar sebelum hilang, atau badge status pesanan yang berganti warna pelan-pelan. Kamu cukup menyebut keadaan awal, keadaan akhir, dan berapa lama perpindahannya. Bayangin papan pesanan di warung Martabak Bang Jali. Kertas pesanan baru ditempel dengan diselipkan dari samping, dan kertas yang sudah selesai gak langsung dicabut: Bang Jali menurunkannya pelan dulu, baru dibuang.
🧪 Tebakan cepat: Baris keranjang memakai
animate.leave="keluar", dan class.keluarmenjalankan animasi CSS 200ms. Pembeli klik Buang. Berapa lama<li>itu masih ada di DOM? Jawabannya ada di bagian "animate.enter dan animate.leave".
animate.enter dan animate.leave
Sejak Angular 20.2, elemen yang masuk dan keluar DOM dianimasikan dengan dua atribut di template, dan animasinya ditulis sebagai CSS biasa. Gak perlu paket tambahan, import, atau provider:
@Component({
selector: 'app-keranjang',
template: `
@for (item of keranjang(); track item.id) {
<li animate.enter="masuk" animate.leave="keluar">
{{ item.nama }} <button (click)="buang(item.id)">Buang</button>
</li>
}
`,
styles: `
.masuk { animation: masuk 250ms ease-out; }
@keyframes masuk { from { opacity: 0; transform: translateX(-16px); } }
.keluar { animation: keluar 200ms ease-in forwards; }
@keyframes keluar { to { opacity: 0; transform: translateX(16px); } }
`,
})
export class KeranjangPage {
keranjang = signal([
{ id: 1, nama: 'Martabak Manis Cokelat Keju' },
{ id: 2, nama: 'Martabak Telur Spesial' },
])
buang(id: number) {
this.keranjang.update((isi) => isi.filter((item) => item.id !== id))
}
}
animate.enter="masuk": saat<li>masuk ke DOM, Angular memasang classmasuk, menunggu animasi CSS-nya selesai, lalu melepas class itu lagi. Baris yang sudah ada saat halaman dibuka juga menjalankannya sekali.animate.leave="keluar": saat<li>harus keluar, Angular memasang classkeluardan menahan elemennya di DOM sampai animasi CSS-nya selesai, baru membuangnya.forwardsdi animasi keluar menahan keadaan akhirnya (opacity 0) sampai elemennya dibuang.- Elemennya harus benar-benar masuk dan keluar DOM: lewat
@if, atau baris@foryang punyatrack. - Nama class-nya bisa diikat ke nilai:
[animate.enter]="kelasMasuk".
Dicoba di editor latihan (Angular 22): setelah Buang diklik, <li>-nya masih ada di DOM sekitar 200 ms dengan class keluar sambil memudar, lalu hilang. Itu jawaban tebakannya. Class masuk pada baris yang baru muncul lepas sendiri begitu animasinya selesai, sekitar 250 ms.
Class tanpa animasi di CSS-nya, misalnya animate.leave="keluar" tapi .keluar gak pernah ditulis, gak memberi Angular apa pun untuk ditunggu: barisnya langsung dibuang, sama seperti tanpa animate.leave.
Kurangi gerakan: prefers-reduced-motion
Sebagian orang pusing melihat gerakan dan menyalakan "kurangi gerakan" di HP atau laptopnya. Hormati pilihan itu di CSS-nya:
@media (prefers-reduced-motion: reduce) {
.masuk, .keluar { animation: none; }
}
Dengan animation: none, gak ada animasi yang ditunggu, jadi baris yang dibuang langsung hilang dan baris baru langsung tampil.
Kode yang sudah ada: @angular/animations
Sebelum 20.2, Angular punya mesin animasi sendiri di paket @angular/animations. Sejak 20.2 seluruh paket itu ditandai deprecated: Use animate.enter or animate.leave instead. Intent to remove in v23. Tapi kamu masih akan sering menemuinya di proyek yang sudah ada, jadi bentuknya perlu dikenali.
Paket itu harus dipasang (npm install @angular/animations), lalu provider-nya didaftarkan sekali di app.config.ts:
// src/app/app.config.ts
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'
export const appConfig: ApplicationConfig = {
providers: [provideAnimationsAsync()],
}
Ini output ng build (Angular 20) untuk aplikasi contoh, satu komponen dengan animasi:
| Provider | Initial total | Diunduh belakangan |
|---|---|---|
| belum ada | 278,16 kB | |
provideAnimations() |
341,33 kB | |
provideAnimationsAsync() |
286,31 kB | chunk browser 64,18 kB |
provideAnimations() memasukkan mesin animasinya ke bundle awal, provideAnimationsAsync() menaruhnya di chunk terpisah yang diunduh setelah halaman tampil. Lupa provider-nya, dan console menulis ERROR RuntimeError: NG05105: Unexpected synthetic property @status found. Editor latihan dan Playground menambahkan provideAnimations() sendiri begitu kodenya memakai @angular/animations.
Animasinya ditulis di TypeScript, di animations: [...] milik @Component. Badge status pesanan Bang Jali:
import { trigger, state, style, transition, animate } from '@angular/animations'
@Component({
selector: 'app-pesanan',
animations: [
trigger('status', [
state('menunggu', style({ backgroundColor: '#e2e8f0' })),
state('dimasak', style({ backgroundColor: '#fde68a' })),
state('selesai', style({ backgroundColor: '#bbf7d0' })),
transition('menunggu => dimasak', animate('300ms ease-out')),
transition('* => selesai', animate('600ms ease-in')),
transition('* => *', animate('200ms')),
]),
],
template: `<span class="badge" [@status]="status()">{{ status() }}</span>`,
})
export class PesananPage {
status = signal('menunggu')
}
trigger('status', [...])memberi nama sebuah animasi, dan[@status]="status()"memasangnya di elemen.state('dimasak', style({...})): style elemen saat nilainya'dimasak'.transition('menunggu => dimasak', animate('300ms ease-out')): kapan animasi jalan dan berapa lama.*artinya state apa pun, dan Angular memakai transition pertama yang cocok, dari atas ke bawah, jadi wildcard ditulis paling akhir.
Elemen yang belum ada di DOM berada di state khusus bernama void. :enter adalah singkatan void => * (masuk ke DOM), :leave adalah singkatan * => void (keluar dari DOM). Baris keranjang tadi, versi lamanya:
trigger('item', [
transition(':enter', [
style({ opacity: 0, transform: 'translateX(-16px)' }),
animate('250ms ease-out'),
]),
transition(':leave', [
animate('200ms ease-in', style({ opacity: 0, transform: 'translateX(16px)' })),
]),
])
// template
@for (item of keranjang(); track item.id) {
<li @item>{{ item.nama }}</li>
}
Hasilnya sama dengan animate.leave: <li> yang keluar ditahan di DOM sampai animasi :leave-nya selesai. Untuk "kurangi gerakan", kode lama memakai [@.disabled]="true", yang mematikan semua animasi di elemen itu dan anak-anaknya.
⚠️ Jebakan yang sering ketemu
- Elemen yang selalu ada di DOM.
animate.leavecuma jalan saat elemennya keluar dari DOM. Mengganti class dengan[class.keluar]di elemen yang selalu ada gak membuat apa pun masuk atau keluar. Bungkus dengan@if, atau render lewat@for. - Class tanpa animasi.
animate.leave="keluar"tanpa.keluar { animation: ... }distylesmembuat elemennya langsung dibuang. - Nama class beda dengan CSS.
animate.enter="fade-in"sementara CSS-nya.fadeIn: gak ada animasi yang jalan. - Kode lama tanpa provider.
@angular/animationstanpaprovideAnimationsAsync()atauprovideAnimations()berhenti denganNG05105: Unexpected synthetic property @status found. - Detail latihan. Starter-nya sudah punya tombol
Toggledan@keyframes fade-insertafade-out. Tulis@if (show) { <p animate.enter="fade-in" animate.leave="fade-out">Animasi fade</p> }di bawah tombolnya, lalu.fade-in { animation: fade-in 300ms ease-in; }dan.fade-out { animation: fade-out 300ms ease-out forwards; }distyles. Penilai mengklikTogglesekali dan melihat paragrafnya masih ada dengan classfade-outyang sedang beranimasi, lalu hilang. Setelah dua klik, paragrafnya harus kembali berbunyiAnimasi fadedengan classfade-inyang sedang beranimasi.
TL;DR: Sejak Angular 20.2, elemen yang masuk dan keluar DOM dianimasikan dengan animate.enter="kelas" dan animate.leave="kelas" di template, dan class-nya menjalankan animasi CSS biasa di styles; gak perlu import atau provider. Angular memasang class masuk lalu melepasnya setelah animasinya selesai, dan menahan elemen yang keluar di DOM sampai animasi keluarnya selesai. Class tanpa animasi membuat elemennya langsung hilang, dan animation: none di @media (prefers-reduced-motion: reduce) menghormati pengaturan "kurangi gerakan". Kode lama memakai @angular/animations (deprecated sejak 20.2): provideAnimationsAsync(), animations: [trigger('nama', [state(...), transition(...)])], [@nama], :enter = void => *, dan :leave = * => void.