Din v20.2, Angular are în @angular/core două instrucțiuni noi: animate.enter și animate.leave. Animația o scrii în CSS, ca orice animație. Angular face singur partea pe care CSS-ul n-o poate face: pune o clasă când elementul intră în DOM și, la ieșire, întârzie scoaterea elementului până se termină animația. În aceeași versiune, pachetul @angular/animations (trigger, state, transition) a fost depreciat.
Pentru tranziții între pagini sau între două stări ale aceluiași ecran există View Transitions API. E un API al browserului, integrat în router cu withViewTransitions().
provideAnimations() sau un DSL în TypeScript. Animațiile sunt CSS obișnuit, accelerat de browser, iar animate.leave rezolvă partea grea, adică animarea unui element care urmează să dispară. Documentația v22 recomandă explicit animate.enter / animate.leave pentru tot codul nou.animate.enter și animate.leave
animate.enter="enter" adaugă clasa enter când elementul e inserat (un @if devine adevărat, un element nou apare în @for) și o scoate când animația CSS se termină. animate.leave="leave" pune clasa la ștergere și păstrează elementul în DOM până se termină animația. Nu sunt directive: sunt instrucțiuni ale compilatorului, deci nu trebuie importate și pot fi folosite și ca host binding.
@for (item of items(); track item.id) {
<li animate.enter="enter" animate.leave="leave">{{ item.label }}</li>
}- Măr
- Pară
- Prună
- Cireașă
Keyframes sau transition
Cu @keyframes e simplu: clasa conține animația, iar Angular așteaptă evenimentul ei de final. Cu transition, documentația atrage atenția asupra unui detaliu: clasa de enter reprezintă starea finală. Starea de plecare o dai cu @starting-style, altfel nu există „de unde” să pornească tranziția.
/* Cu transition în loc de keyframes: clasa e starea FINALĂ, iar @starting-style e starea de plecare */
.panel { transition: opacity 250ms, transform 250ms; }
.panel-enter {
opacity: 1;
transform: none;
@starting-style {
opacity: 0;
transform: translateY(12px);
}
}Forma cu funcție: control complet
(animate.enter)="fn($event)" și (animate.leave)="fn($event)" apelează o metodă cu un AnimationCallbackEvent: target (elementul) și animationComplete(). Așa integrezi Web Animations API sau biblioteci ca GSAP. La leave, apelul animationComplete() e obligatoriu. Dacă lipsește, Angular scoate elementul abia după MAX_ANIMATION_TIMEOUT, care e de 4 secunde implicit.
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);
// OBLIGATORIU la leave: altfel Angular scoate elementul abia după MAX_ANIMATION_TIMEOUT (4 s implicit)
animation.finished.then(() => event.animationComplete());
}
}- Jurnalul hook-urilor apare aici.
Migrarea de la @angular/animations
Majoritatea animațiilor cu trigger('fade', [transition(':enter', …)]) se traduc direct în două clase CSS și animate.enter / animate.leave. Stările (state('open')) devin clase CSS normale, legate cu [class.open]. Cele două sisteme pot coexista în aplicație, dar nu în aceeași componentă: documentația spune că rezultatul ar fi clase care rămân pe elemente sau noduri care nu mai sunt scoase. Același lucru e valabil la proiecția de conținut între componente care folosesc sisteme diferite.
// ÎNAINTE — @angular/animations (depreciat din 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> }`,
})
// DUPĂ — CSS + animate.enter / animate.leave, fără pachet și fără 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
Browserul fotografiază pagina, tu schimbi DOM-ul, apoi browserul animă diferența: implicit un cross-fade, iar elementele cu același view-transition-name se „transformă” unul în altul (poziție, mărime). În router, activezi asta cu withViewTransitions(). Funcția e marcată developer preview în tipurile routerului și are opțiunile skipInitialTransition și onViewTransitionCreated. În afara routerului apelezi direct document.startViewTransition(). Într-o aplicație zoneless trebuie să actualizezi DOM-ul sincron, în callback.
import { provideRouter, withViewTransitions } from '@angular/router';
export const appConfig: ApplicationConfig = {
providers: [
provideRouter(
routes,
withViewTransitions({
skipInitialTransition: true, // fără animație la prima încărcare
onViewTransitionCreated: ({ transition }) => {
// rulează în context de injecție; poți sări peste anumite navigări:
// if (...) transition.skipTransition();
},
}),
),
],
};Browserul tău nu suportă View Transitions: schimbarea se face instant (fallback).
Performanță și accesibilitate
- Animă
transformșiopacity: le face compositorul, fără layout.width,height,toprecalculează layout-ul la fiecare cadru. - Durate scurte pentru UI (150–300 ms). Animațiile lungi fac interfața să pară lentă.
- Respectă
prefers-reduced-motion. Site-ul ăsta o face global; de aceea, cu „reduce motion” activ în sistem, demo-urile de mai sus sunt aproape instantanee.
@media (prefers-reduced-motion: reduce) {
.enter, .leave { animation-duration: 1ms; }
::view-transition-group(*) { animation: none; }
}Elementele care intră și ies dintr-o listă se animă cu animate.enter pe fiecare rând al lui @for (vezi Control flow: @if, @for, @switch). Pe liste foarte lungi, animă doar ce e pe ecran: Liste uriașe: virtual scroll. Ce costă o animație și cum măsori, la Performanță — checklist.
Când DA, când NU
animate.enter/animate.leavepentru apariția și dispariția elementelor: liste, toast-uri, panouri, dialoguri.- Forma cu funcție când ai nevoie de JavaScript: animații calculate, GSAP, secvențe.
- View Transitions pentru navigări și schimbări de layout (listă → detaliu), cu fallback instant.
- CSS simplu (
:hover,transition) pentru tot ce nu implică inserare sau ștergere.
@angular/animationsîn cod nou: e depreciat. Păstrează-l doar unde există deja, până migrezi.- Animații pe proprietăți de layout în liste mari: sacadează.
- Animații care blochează interacțiunea: utilizatorul așteaptă să se termine ca să poată da click.
- View Transitions pe fiecare mică schimbare de stare: devine obositor și consumă resurse.
Capcane frecvente
animationComplete()uitat în(animate.leave): elementul rămâne vizibil încă 4 secunde.transitionfără@starting-stylela enter: nu se animă nimic, pentru că nu există stare de plecare.animate.leaveîntr-o componentă copil când e scos părintele. Documentația precizează că animațiile de leave ale copiilor rulează doar pentru elementele din același template. Puneanimate.leavepe elementul host al copilului, în template-ul părintelui.- Cele două sisteme în aceeași componentă (sau prin proiecție de conținut): comportament nedefinit.
startViewTransitionfără actualizare sincronă într-o aplicație zoneless: browserul fotografiază starea veche de două ori și nu se animă nimic.
Fișă rapidă
ENTER/LEAVE (din v20.2, în @angular/core — fără pachet separat)
animate.enter="clasa" clasa e pusă la inserare și scoasă la finalul animației
animate.leave="clasa" elementul rămâne în DOM până se termină animația
[animate.enter]="expr" string, array de clase, semnal
(animate.leave)="fn($event)" AnimationCallbackEvent: target + animationComplete() — OBLIGATORIU la leave
MAX_ANIMATION_TIMEOUT cât așteaptă Angular un leave neterminat (implicit 4 s)
CSS
keyframes: clasa conține animația · transition: clasa = starea finală + @starting-style = start
animă transform și opacity (compositor), nu width/height/top (layout)
prefers-reduced-motion — respectă-l
VIEW TRANSITIONS
provideRouter(routes, withViewTransitions({ skipInitialTransition, onViewTransitionCreated })) — developer preview
view-transition-name: x · ::view-transition-old/new(x) · document.startViewTransition(cb)
@angular/animations depreciat din v20.2 — doar pentru cod existent; nu-l amesteca în aceeași componentă cu animate.*