Seit v20.2 hat Angular in @angular/core zwei neue Anweisungen: animate.enter und animate.leave. Die Animation schreibst du in CSS, wie jede andere Animation. Angular übernimmt den Teil, den CSS allein nicht kann: Es setzt eine Klasse, wenn das Element in den DOM kommt, und verzögert beim Verlassen das Entfernen des Elements, bis die Animation fertig ist. In derselben Version wurde das Paket @angular/animations (trigger, state, transition) als deprecated markiert.
Für Übergänge zwischen Seiten oder zwischen zwei Zuständen desselben Bildschirms gibt es die View Transitions API. Das ist eine Browser-API, die mit withViewTransitions() in den Router integriert ist.
provideAnimations() und keine DSL in TypeScript mehr. Animationen sind gewöhnliches CSS, vom Browser beschleunigt, und animate.leave löst den schwierigen Teil, nämlich ein Element zu animieren, das gleich verschwindet. Die v22-Dokumentation empfiehlt ausdrücklich animate.enter / animate.leave für allen neuen Code.animate.enter und animate.leave
animate.enter="enter" fügt die Klasse enter hinzu, wenn das Element eingefügt wird (ein @if wird wahr, ein neues Element erscheint in einem @for), und entfernt sie, wenn die CSS-Animation endet. animate.leave="leave" setzt die Klasse beim Entfernen und hält das Element im DOM, bis die Animation endet. Das sind keine Direktiven, sondern Compiler-Anweisungen: Du musst sie nicht importieren und kannst sie auch als Host-Bindings verwenden.
@for (item of items(); track item.id) {
<li animate.enter="enter" animate.leave="leave">{{ item.label }}</li>
}- Apfel
- Birne
- Pflaume
- Kirsche
Keyframes oder Transition
Mit @keyframes ist es einfach: Die Klasse enthält die Animation, und Angular wartet auf deren End-Event. Bei transition weist die Dokumentation auf ein Detail hin: Die Enter-Klasse stellt den Endzustand dar. Den Startzustand gibst du mit @starting-style an, sonst gibt es kein „Von“, von dem die Transition ausgehen könnte.
/* Mit transition statt keyframes: Die Klasse ist der ENDzustand, und @starting-style ist der Startzustand */
.panel { transition: opacity 250ms, transform 250ms; }
.panel-enter {
opacity: 1;
transform: none;
@starting-style {
opacity: 0;
transform: translateY(12px);
}
}Die Funktionsform: volle Kontrolle
(animate.enter)="fn($event)" und (animate.leave)="fn($event)" rufen eine Methode mit einem AnimationCallbackEvent auf: target (das Element) und animationComplete(). So bindest du die Web Animations API oder Bibliotheken wie GSAP ein. Bei leave ist der Aufruf von animationComplete() Pflicht. Fehlt er, entfernt Angular das Element erst nach MAX_ANIMATION_TIMEOUT, standardmäßig 4 Sekunden.
import { AnimationCallbackEvent, Component, signal } from '@angular/core';
@Component({
selector: 'app-toasts',
template: `
@for (t of toasts(); track t.id) {
<div class="toast" (animate.enter)="slideIn($event)" (animate.leave)="slideOut($event)">{{ t.text }}</div>
}
`,
})
export class Toasts {
readonly toasts = signal<Toast[]>([]);
slideIn(event: AnimationCallbackEvent): void {
event.target.animate([{ opacity: 0, transform: 'translateX(40px)' }, { opacity: 1, transform: 'none' }], 250);
}
slideOut(event: AnimationCallbackEvent): void {
const animation = event.target.animate([{ opacity: 1 }, { opacity: 0, transform: 'translateX(40px)' }], 300);
// PFLICHT bei leave: sonst entfernt Angular das Element erst nach MAX_ANIMATION_TIMEOUT (standardmäßig 4 s)
animation.finished.then(() => event.animationComplete());
}
}- Das Hook-Protokoll erscheint hier.
Migration von @angular/animations
Die meisten Animationen mit trigger('fade', [transition(':enter', …)]) lassen sich direkt in zwei CSS-Klassen und animate.enter / animate.leave übersetzen. Zustände (state('open')) werden zu gewöhnlichen CSS-Klassen, gebunden mit [class.open]. Beide Systeme können in einer Anwendung nebeneinander bestehen, aber nicht in derselben Komponente: Laut Dokumentation bleiben sonst Klassen an Elementen hängen, oder Knoten werden nie entfernt. Dasselbe gilt für Content Projection zwischen Komponenten, die unterschiedliche Systeme verwenden.
// VORHER — @angular/animations (deprecated seit v20.2)
@Component({
animations: [
trigger('fade', [
transition(':enter', [style({ opacity: 0 }), animate('200ms', style({ opacity: 1 }))]),
transition(':leave', [animate('200ms', style({ opacity: 0 }))]),
]),
],
template: `@if (open()) { <div @fade>…</div> }`,
})
// NACHHER — CSS + animate.enter / animate.leave, ohne Paket und ohne provideAnimations()
@Component({
template: `@if (open()) { <div animate.enter="fade-in" animate.leave="fade-out">…</div> }`,
styles: `
.fade-in { animation: fade 200ms; }
.fade-out { animation: fade 200ms reverse forwards; }
@keyframes fade { from { opacity: 0; } }
`,
})View Transitions
Der Browser macht einen Snapshot der Seite, du änderst den DOM, und dann animiert der Browser die Differenz: standardmäßig ein Cross-Fade, und Elemente mit demselben view-transition-name „verwandeln“ sich ineinander (Position, Größe). Im Router aktivierst du das mit withViewTransitions(). Die Funktion ist in den Typen des Routers als Developer Preview markiert und hat die Optionen skipInitialTransition und onViewTransitionCreated. Außerhalb des Routers rufst du document.startViewTransition() direkt auf. In einer Zoneless-Anwendung musst du den DOM synchron im Callback aktualisieren.
import { provideRouter, withViewTransitions } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withViewTransitions({
skipInitialTransition: true, // keine Animation beim ersten Laden
onViewTransitionCreated: ({ transition }) => {
// läuft in einem Injection-Kontext; bestimmte Navigationen kannst du überspringen:
// if (...) transition.skipTransition();
},
}),
),
],
};Dein Browser unterstützt keine View Transitions: Die Änderung erfolgt sofort (Fallback).
Performance und Barrierefreiheit
- Animiere
transformundopacity: Der Compositor übernimmt das, ohne Layout.width,heightundtopberechnen das Layout in jedem Frame neu. - Kurze Dauern für die UI (150–300 ms). Lange Animationen lassen die Oberfläche träge wirken.
- Respektiere
prefers-reduced-motion. Diese Seite macht das global; deshalb sind die Beispiele oben bei aktivierter Einstellung „Bewegung reduzieren“ im System fast sofort fertig.
@media (prefers-reduced-motion: reduce) {
.enter, .leave { animation-duration: 1ms; }
::view-transition-group(*) { animation: none; }
}Elemente, die in eine Liste kommen oder sie verlassen, animierst du mit animate.enter auf jeder @for-Zeile (siehe Control Flow: @if, @for, @switch). Bei sehr langen Listen animiere nur, was auf dem Bildschirm ist: Riesige Listen: Virtual Scroll. Was eine Animation kostet und wie du es misst: Performance — eine Checkliste.
Wann ja, wann nein
animate.enter/animate.leavefür Elemente, die erscheinen und verschwinden: Listen, Toasts, Panels, Dialoge.- Die Funktionsform, wenn du JavaScript brauchst: berechnete Animationen, GSAP, Sequenzen.
- View Transitions für Navigationen und Layout-Änderungen (Liste → Detail), mit sofortigem Fallback.
- Reines CSS (
:hover,transition) für alles, was kein Einfügen oder Entfernen betrifft.
@angular/animationsin neuem Code: Es ist deprecated. Behalte es nur dort, wo es schon existiert, bis du migrierst.- Animationen auf Layout-Eigenschaften in großen Listen: Sie ruckeln.
- Animationen, die die Interaktion blockieren: Der Benutzer wartet, bis sie fertig sind, bevor er klicken kann.
- View Transitions bei jeder kleinen Zustandsänderung: Das wird anstrengend und kostet Ressourcen.
Häufige Stolperfallen
animationComplete()vergessen in(animate.leave): Das Element bleibt weitere 4 Sekunden sichtbar.transitionohne@starting-stylebei enter: Nichts wird animiert, weil es keinen Startzustand gibt.animate.leavein einer Kindkomponente, wenn der Parent entfernt wird. Die Dokumentation stellt klar, dass die Leave-Animationen von Kindern nur für Elemente im selben Template laufen. Setzeanimate.leaveauf das Host-Element des Kindes, im Template des Parents.- Beide Systeme in derselben Komponente (oder über Content Projection): undefiniertes Verhalten.
startViewTransitionohne synchrone Aktualisierung in einer Zoneless-Anwendung: Der Browser macht den Snapshot des alten Zustands zweimal, und nichts wird animiert.
Spickzettel
ENTER/LEAVE (seit v20.2, in @angular/core — kein separates Paket)
animate.enter="Klasse" die Klasse wird beim Einfügen gesetzt und am Ende der Animation entfernt
animate.leave="Klasse" das Element bleibt im DOM, bis die Animation endet
[animate.enter]="expr" String, Array von Klassen, Signal
(animate.leave)="fn($event)" AnimationCallbackEvent: target + animationComplete() — PFLICHT bei leave
MAX_ANIMATION_TIMEOUT wie lange Angular auf ein unfertiges leave wartet (Standard 4 s)
CSS
keyframes: Die Klasse enthält die Animation · transition: Klasse = Endzustand + @starting-style = Start
animiere transform und opacity (Compositor), nicht width/height/top (Layout)
prefers-reduced-motion — respektiere es
VIEW TRANSITIONS
provideRouter(routes, withViewTransitions({ skipInitialTransition, onViewTransitionCreated })) — Developer Preview
view-transition-name: x · ::view-transition-old/new(x) · document.startViewTransition(cb)
@angular/animations deprecated seit v20.2 — nur für bestehenden Code; nicht in derselben Komponente mit animate.* mischen