Depuis la v20.2, Angular propose deux nouvelles instructions dans @angular/core : animate.enter et animate.leave. Vous écrivez l’animation en CSS, comme n’importe quelle autre animation. Angular fait la partie que le CSS ne sait pas faire seul : il ajoute une classe quand l’élément entre dans le DOM et, à la sortie, retarde le retrait de l’élément jusqu’à la fin de l’animation. Dans la même version, le package @angular/animations (trigger, state, transition) a été déprécié.
Pour les transitions entre pages, ou entre deux états d’un même écran, il y a la View Transitions API. C’est une API du navigateur, intégrée au routeur avec withViewTransitions().
provideAnimations() ni d’un DSL en TypeScript. Les animations sont du CSS ordinaire, accéléré par le navigateur, et animate.leave résout la partie difficile : animer un élément sur le point de disparaître. La documentation de la v22 recommande explicitement animate.enter / animate.leave pour tout nouveau code.animate.enter et animate.leave
animate.enter="enter" ajoute la classe enter quand l’élément est inséré (un @if devient vrai, un nouvel élément apparaît dans un @for) et la retire à la fin de l’animation CSS. animate.leave="leave" pose la classe au retrait et garde l’élément dans le DOM jusqu’à la fin de l’animation. Ce ne sont pas des directives : ce sont des instructions du compilateur, donc elles n’ont pas besoin d’être importées et peuvent aussi servir de host bindings.
@for (item of items(); track item.id) {
<li animate.enter="enter" animate.leave="leave">{{ item.label }}</li>
}- Pomme
- Poire
- Prune
- Cerise
Keyframes ou transition
Avec @keyframes, c’est simple : la classe contient l’animation, et Angular attend son événement de fin. Avec transition, la documentation signale un détail : la classe d’entrée représente l’état final. Vous donnez l’état de départ avec @starting-style, sinon la transition n’a pas de « from » d’où partir.
/* Avec transition au lieu de keyframes : la classe est l’état FINAL, et @starting-style est l’état de départ */
.panel { transition: opacity 250ms, transform 250ms; }
.panel-enter {
opacity: 1;
transform: none;
@starting-style {
opacity: 0;
transform: translateY(12px);
}
}La forme fonction : contrôle total
(animate.enter)="fn($event)" et (animate.leave)="fn($event)" appellent une méthode avec un AnimationCallbackEvent : target (l’élément) et animationComplete(). C’est ainsi que vous intégrez la Web Animations API ou des bibliothèques comme GSAP. Au leave, l’appel à animationComplete() est obligatoire. S’il manque, Angular ne retire l’élément qu’après MAX_ANIMATION_TIMEOUT, soit 4 secondes par défaut.
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);
// OBLIGATOIRE au leave : sinon Angular ne retire l’élément qu’après MAX_ANIMATION_TIMEOUT (4 s par défaut)
animation.finished.then(() => event.animationComplete());
}
}- Le journal des hooks apparaît ici.
Migrer depuis @angular/animations
La plupart des animations écrites avec trigger('fade', [transition(':enter', …)]) se traduisent directement en deux classes CSS et animate.enter / animate.leave. Les états (state('open')) deviennent de simples classes CSS, liées avec [class.open]. Les deux systèmes peuvent coexister dans une application, mais pas dans le même composant : la documentation indique que le résultat serait des classes qui restent sur les éléments, ou des nœuds jamais retirés. Il en va de même pour la projection de contenu entre composants qui utilisent des systèmes différents.
// AVANT — @angular/animations (déprécié depuis la 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> }`,
})
// APRÈS — CSS + animate.enter / animate.leave, sans package et sans 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
Le navigateur prend un instantané de la page, vous modifiez le DOM, puis le navigateur anime la différence : par défaut un fondu enchaîné, et les éléments ayant le même view-transition-name se « transforment » l’un en l’autre (position, taille). Dans le routeur, vous l’activez avec withViewTransitions(). La fonction est marquée developer preview dans les types du routeur et propose les options skipInitialTransition et onViewTransitionCreated. En dehors du routeur, vous appelez directement document.startViewTransition(). Dans une application zoneless, vous devez mettre à jour le DOM de façon synchrone, dans le callback.
import { provideRouter, withViewTransitions } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withViewTransitions({
skipInitialTransition: true, // pas d’animation au premier chargement
onViewTransitionCreated: ({ transition }) => {
// s’exécute dans un contexte d’injection ; vous pouvez ignorer certaines navigations :
// if (...) transition.skipTransition();
},
}),
),
],
};Votre navigateur ne prend pas en charge les View Transitions : le changement est instantané (fallback).
Performance et accessibilité
- Animez
transformetopacity: le compositeur s’en charge, sans layout.width,heightettoprecalculent le layout à chaque frame. - Des durées courtes pour l’UI (150–300 ms). Les animations longues donnent une impression de lenteur.
- Respectez
prefers-reduced-motion. Ce site le fait globalement ; c’est pourquoi, avec « réduire les animations » activé dans le système, les démos ci-dessus sont presque instantanées.
@media (prefers-reduced-motion: reduce) {
.enter, .leave { animation-duration: 1ms; }
::view-transition-group(*) { animation: none; }
}Les éléments qui entrent dans une liste ou en sortent s’animent avec animate.enter sur chaque ligne du @for (voir Control Flow : @if, @for, @switch). Sur de très longues listes, n’animez que ce qui est à l’écran : Listes géantes : virtual scroll. Ce que coûte une animation et comment la mesurer : Performance — une checklist.
Quand OUI, quand NON
animate.enter/animate.leavepour les éléments qui apparaissent et disparaissent : listes, toasts, panneaux, dialogues.- La forme fonction quand vous avez besoin de JavaScript : animations calculées, GSAP, séquences.
- View Transitions pour les navigations et les changements de mise en page (liste → détail), avec un fallback instantané.
- Du CSS simple (
:hover,transition) pour tout ce qui n’implique ni insertion ni retrait.
@angular/animationsdans du nouveau code : il est déprécié. Ne le gardez que là où il existe déjà, jusqu’à la migration.- Des animations sur des propriétés de layout dans de grandes listes : elles saccadent.
- Des animations qui bloquent l’interaction : l’utilisateur attend qu’elles se terminent avant de pouvoir cliquer.
- Des View Transitions à chaque petit changement d’état : c’est fatigant et gourmand en ressources.
Pièges fréquents
animationComplete()oublié dans(animate.leave): l’élément reste visible 4 secondes de plus.transitionsans@starting-styleà l’entrée : rien ne s’anime, faute d’état de départ.animate.leavedans un composant enfant quand le parent est retiré. La documentation précise que les animations de sortie des enfants ne s’exécutent que pour les éléments du même template. Placezanimate.leavesur l’élément hôte de l’enfant, dans le template du parent.- Les deux systèmes dans le même composant (ou via la projection de contenu) : comportement indéfini.
startViewTransitionsans mise à jour synchrone dans une application zoneless : le navigateur prend deux fois l’instantané de l’ancien état et rien ne s’anime.
Aide-mémoire
ENTER/LEAVE (depuis la v20.2, dans @angular/core — pas de package séparé)
animate.enter="classe" la classe est ajoutée à l’insertion et retirée à la fin de l’animation
animate.leave="classe" l’élément reste dans le DOM jusqu’à la fin de l’animation
[animate.enter]="expr" chaîne, tableau de classes, signal
(animate.leave)="fn($event)" AnimationCallbackEvent: target + animationComplete() — OBLIGATOIRE au leave
MAX_ANIMATION_TIMEOUT durée d’attente d’Angular pour un leave inachevé (4 s par défaut)
CSS
keyframes : la classe contient l’animation · transition : classe = état final + @starting-style = départ
animez transform et opacity (compositeur), pas width/height/top (layout)
prefers-reduced-motion — respectez-le
VIEW TRANSITIONS
provideRouter(routes, withViewTransitions({ skipInitialTransition, onViewTransitionCreated })) — developer preview
view-transition-name: x · ::view-transition-old/new(x) · document.startViewTransition(cb)
@angular/animations déprécié depuis la v20.2 — uniquement pour le code existant ; ne le mélangez pas avec animate.* dans le même composant