El paquete @angular/animations está deprecado a partir de la v20.2, que también introdujo las nuevas características animate.enter y animate.leave para agregar animaciones a tu aplicación. Usando estas nuevas características, puedes reemplazar todas las animaciones basadas en @angular/animations con CSS puro o bibliotecas de animación JS. Eliminar @angular/animations de tu aplicación puede reducir significativamente el tamaño de tu bundle de JavaScript. Las animaciones CSS nativas generalmente ofrecen rendimiento superior, ya que pueden beneficiarse de la aceleración por hardware. Esta guía te acompaña a través del proceso de refactorizar tu código de @angular/animations a animaciones CSS nativas.
Cómo escribir animaciones en CSS nativo
Si nunca has escrito animaciones en CSS nativo, hay varias guías excelentes para comenzar. Aquí hay algunas de ellas: Guía de animaciones CSS de MDN Guía de animaciones CSS3 de W3Schools Tutorial completo de animaciones CSS Animación CSS para principiantes
y un par de videos: Aprende animación CSS en 9 minutos Lista de reproducción de tutorial de animación CSS de Net Ninja
Consulta algunas de estas diversas guías y tutoriales, y luego regresa a esta guía.
Creando animaciones reutilizables
Al igual que con el paquete de animaciones, puedes crear animaciones reutilizables que se pueden compartir en toda tu aplicación. La versión del paquete de animaciones de esto te hacía usar la función animation() en un archivo TypeScript compartido. La versión CSS nativa de esto es similar, pero vive en un archivo CSS compartido.
Con paquete de Animations
src/app/animations.ts
import {animation, style, animate, trigger, transition, useAnimation} from '@angular/animations';export const transitionAnimation = animation([ style({ height: '{{ height }}', opacity: '{{ opacity }}', backgroundColor: '{{ backgroundColor }}', }), animate('{{ time }}'),]);export const sharedAnimation = animation([ style({ height: 0, opacity: 1, backgroundColor: 'red', }), animate('1s'),]);export const triggerAnimation = trigger('openClose', [ transition('open => closed', [ useAnimation(transitionAnimation, { params: { height: 0, opacity: 1, backgroundColor: 'red', time: '1s', }, }), ]),]);
Con CSS nativo
src/app/animations.css
@keyframes sharedAnimation { to { height: 0; opacity: 1; background-color: 'red'; }}.animated-class { animation: sharedAnimation 1s;}.open { height: '200px'; opacity: 1; background-color: 'yellow'; transition: all 1s;}.closed { height: '100px'; opacity: 0.8; background-color: 'blue'; transition: all 1s;}.example-element { animation-duration: 1s; animation-delay: 500ms; animation-timing-function: ease-in-out;}.example-shorthand { animation: exampleAnimation 1s ease-in-out 500ms;}.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;}
Agregar la clase animated-class a un elemento activaría la animación en ese elemento.
Animando una transición
Animando estado y estilos
El paquete de animaciones te permitía definir varios estados usando la función state() dentro de un componente. Los ejemplos podrían ser un estado open o closed que contiene los estilos para cada estado respectivo dentro de la definición. Por ejemplo:
Con paquete de Animations
src/app/open-close.component.ts
import {Component, input} from '@angular/core';import {trigger, transition, state, animate, style, AnimationEvent} 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.8, backgroundColor: 'blue', }), ), transition('open => closed', [animate('1s')]), transition('closed => open', [animate('0.5s')]), transition('* => closed', [animate('1s')]), transition('* => open', [animate('0.5s')]), transition('open <=> closed', [animate('0.5s')]), transition('* => open', [animate('1s', style({opacity: '*'}))]), transition('* => *', [animate('1s')]), ]), ], templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { logging = input(false); isOpen = true; toggle() { this.isOpen = !this.isOpen; } onAnimationEvent(event: AnimationEvent) { if (!this.logging) { return; } // openClose is trigger name in this example console.warn(`Animation Trigger: ${event.triggerName}`); // phaseName is "start" or "done" console.warn(`Phase: ${event.phaseName}`); // in our example, totalTime is 1000 (number of milliseconds in a second) console.warn(`Total time: ${event.totalTime}`); // in our example, fromState is either "open" or "closed" console.warn(`From: ${event.fromState}`); // in our example, toState either "open" or "closed" console.warn(`To: ${event.toState}`); // the HTML element itself, the button in this case console.warn(`Element: ${event.element}`); }}
Este mismo comportamiento se puede lograr nativamente usando clases CSS ya sea mediante una animación de keyframe o estilo de transición.
Con CSS nativo
src/app/animations.css
@keyframes sharedAnimation { to { height: 0; opacity: 1; background-color: 'red'; }}.animated-class { animation: sharedAnimation 1s;}.open { height: '200px'; opacity: 1; background-color: 'yellow'; transition: all 1s;}.closed { height: '100px'; opacity: 0.8; background-color: 'blue'; transition: all 1s;}.example-element { animation-duration: 1s; animation-delay: 500ms; animation-timing-function: ease-in-out;}.example-shorthand { animation: exampleAnimation 1s ease-in-out 500ms;}.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;}
Disparar el estado open o closed se hace alternando clases en el elemento en tu componente. Puedes encontrar ejemplos de cómo hacer esto en nuestra guía de plantillas.
Puedes ver ejemplos similares en la guía de plantillas para animar estilos directamente.
Transiciones, tiempo y easing
La función animate() del paquete de animaciones permite proporcionar tiempo, como duración, retrasos y easing. Esto se puede hacer nativamente con CSS usando varias propiedades CSS o propiedades abreviadas.
Especifica animation-duration, animation-delay y animation-timing-function para una animación de keyframe en CSS, o alternativamente usa la propiedad abreviada animation.
src/app/animations.css
@keyframes sharedAnimation { to { height: 0; opacity: 1; background-color: 'red'; }}.animated-class { animation: sharedAnimation 1s;}.open { height: '200px'; opacity: 1; background-color: 'yellow'; transition: all 1s;}.closed { height: '100px'; opacity: 0.8; background-color: 'blue'; transition: all 1s;}.example-element { animation-duration: 1s; animation-delay: 500ms; animation-timing-function: ease-in-out;}.example-shorthand { animation: exampleAnimation 1s ease-in-out 500ms;}.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;}
De manera similar, puedes usar transition-duration, transition-delay y transition-timing-function y la abreviación transition para animaciones que no están usando @keyframes.
src/app/animations.css
@keyframes sharedAnimation { to { height: 0; opacity: 1; background-color: 'red'; }}.animated-class { animation: sharedAnimation 1s;}.open { height: '200px'; opacity: 1; background-color: 'yellow'; transition: all 1s;}.closed { height: '100px'; opacity: 0.8; background-color: 'blue'; transition: all 1s;}.example-element { animation-duration: 1s; animation-delay: 500ms; animation-timing-function: ease-in-out;}.example-shorthand { animation: exampleAnimation 1s ease-in-out 500ms;}.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;}
Disparando una animación
El paquete de animaciones requería especificar triggers usando la función trigger() y anidar todos tus estados dentro de ella. Con CSS nativo, esto es innecesario. Las animaciones se pueden disparar alternando estilos o clases CSS. Una vez que una clase está presente en un elemento, la animación ocurrirá. Eliminar la clase revertirá el elemento a cualquier CSS que esté definido para ese elemento. Esto resulta en significativamente menos código para hacer la misma animación. Aquí hay un ejemplo:
Con paquete de Animations
src/app/open-close.component.ts
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.component.html', styleUrl: 'open-close.component.css',})export class OpenCloseComponent { isOpen = signal(false); toggle() { this.isOpen.update((isOpen) => !isOpen); }}
src/app/open-close.component.html
<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>
src/app/open-close.component.css
: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;}
Con CSS nativo
src/app/open-close.component.ts
import {Component, signal} from '@angular/core';@Component({ selector: 'app-open-close', templateUrl: 'open-close.component.html', styleUrls: ['open-close.component.css'],})export class OpenCloseComponent { isOpen = signal(true); toggle() { this.isOpen.update((isOpen) => !isOpen); }}
src/app/open-close.component.html
<h2>Open / Close Example</h2><button type="button" (click)="toggle()">Toggle Open/Close</button><div class="open-close-container" [class.open]="isOpen()"> <p>The box is now {{ isOpen() ? 'Open' : 'Closed' }}!</p></div>
src/app/open-close.component.css
: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-color: blue; color: #ebebeb; transition-property: height, opacity, background-color, color; transition-duration: 1s;}.open { transition-duration: 0.5s; height: 200px; opacity: 1; background-color: yellow; color: #000000;}
Transiciones y triggers
Coincidencia de estado predefinido y comodines
El paquete de animaciones ofrece la capacidad de hacer coincidir tus estados definidos con una transición mediante cadenas. Por ejemplo, animar de open a closed sería open => closed. Puedes usar comodines para hacer coincidir cualquier estado con un estado objetivo, como * => closed y la palabra clave void se puede usar para estados de entrada y salida. Por ejemplo: * => void para cuando un elemento sale de una vista o void => * para cuando el elemento entra en una vista.
Estos patrones de coincidencia de estado no se necesitan en absoluto cuando se anima con CSS directamente. Puedes gestionar qué transiciones y animaciones @keyframes se aplican basándote en las clases que establezcas y/o los estilos que establezcas en los elementos. También puedes agregar @starting-style para controlar cómo se ve el elemento al entrar inmediatamente al DOM.
Cálculo automático de propiedades con comodines
El paquete de animaciones ofrece la capacidad de animar cosas que han sido históricamente difíciles de animar, como animar una altura establecida a height: auto. Ahora también puedes hacer esto con CSS puro.
Con paquete de Animations
src/app/auto-height.component.ts
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.component.html', styleUrl: 'auto-height.component.css',})export class AutoHeightComponent { isOpen = signal(false); toggle() { this.isOpen.update((isOpen) => !isOpen); }}
src/app/auto-height.component.html
<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>
src/app/auto-height.component.css
.container { display: block; overflow: hidden;}.container .content { padding: 20px; margin-top: 1em; font-weight: bold; font-size: 20px; background-color: blue; color: #ebebeb;}
Puedes usar css-grid para animar a altura automática.
Con CSS nativo
src/app/auto-height.component.ts
import {Component, signal} from '@angular/core';@Component({ selector: 'app-auto-height', templateUrl: 'auto-height.component.html', styleUrls: ['auto-height.component.css'],})export class AutoHeightComponent { isOpen = signal(true); toggle() { this.isOpen.update((isOpen) => !isOpen); }}
src/app/auto-height.component.html
<h2>Auto Height Example</h2><button type="button" (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>
src/app/auto-height.component.css
.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-color: blue; color: #ebebeb; overflow: hidden;}.container.open .content { visibility: visible;}
Si no tienes que preocuparte por soportar todos los navegadores, también puedes revisar calc-size(), que es la verdadera solución para animar altura automática. Consulta la documentación de MDN y (este tutorial)[https://frontendmasters.com/blog/one-of-the-boss-battles-of-css-is-almost-won-transitioning-to-auto/] para más información.
Animar entrada y salida de una vista
El paquete de animaciones ofrecía el patrón de coincidencia mencionado anteriormente para entrada y salida, pero también incluía los alias abreviados de :enter y :leave.
Con paquete de Animations
src/app/insert-remove.component.ts
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.component.html', styleUrls: ['insert-remove.component.css'],})export class InsertRemoveComponent { isShown = false; toggle() { this.isShown = !this.isShown; }}
src/app/insert-remove.component.html
<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>}
src/app/insert-remove.component.css
: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;}
Aquí está cómo se puede lograr lo mismo sin el paquete de animaciones usando animate.enter.
Con CSS nativo
src/app/insert.component.ts
import {Component, signal} from '@angular/core';@Component({ selector: 'app-insert', templateUrl: 'insert.component.html', styleUrls: ['insert.component.css'],})export class InsertComponent { isShown = signal(false); toggle() { this.isShown.update((isShown) => !isShown); }}
src/app/insert.component.html
<h2>Insert Element Example</h2><nav> <button type="button" (click)="toggle()">Toggle Element</button></nav>@if (isShown()) { <div class="insert-container" animate.enter="enter-animation"> <p>The box is inserted</p> </div>}
src/app/insert.component.css
:host { display: block;}.insert-container { border: 1px solid #dddddd; margin-top: 1em; padding: 20px 20px 0px 20px; font-weight: bold; font-size: 20px;}.enter-animation { animation: slide-fade 1s;}@keyframes slide-fade { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); }}
Usa animate.leave para animar elementos a medida que salen de la vista, lo que aplicará las clases CSS especificadas al elemento a medida que sale de la vista.
Con CSS nativo
src/app/remove.component.ts
import {Component, signal} from '@angular/core';@Component({ selector: 'app-remove', templateUrl: 'remove.component.html', styleUrls: ['remove.component.css'],})export class RemoveComponent { isShown = signal(false); toggle() { this.isShown.update((isShown) => !isShown); }}
src/app/remove.component.html
<h2>Remove Element Example</h2><nav> <button type="button" (click)="toggle()">Toggle Element</button></nav>@if (isShown()) { <div class="insert-container" animate.leave="deleting"> <p>The box is inserted</p> </div>}
src/app/remove.component.css
:host { display: block;}.insert-container { border: 1px solid #dddddd; margin-top: 1em; padding: 20px 20px 0px 20px; font-weight: bold; font-size: 20px; opacity: 1; transition: opacity 200ms ease-in; @starting-style { opacity: 0; }}.deleting { opacity: 0; transform: translateY(20px); transition: opacity 500ms ease-out, transform 500ms ease-out;}
Para más información sobre animate.enter y animate.leave, consulta la guía de animaciones de entrada y salida.
Animando incremento y decremento
Junto con los mencionados :enter y :leave, también está :increment y :decrement. También puedes animar estos agregando y eliminando clases. A diferencia de los alias integrados del paquete de animaciones, no hay aplicación automática de clases cuando los valores suben o bajan. Puedes aplicar las clases apropiadas programáticamente. Aquí hay un ejemplo:
Con paquete de Animations
src/app/increment-decrement.component.ts
import {Component, signal} from '@angular/core';import {trigger, transition, animate, style, query, stagger} from '@angular/animations';@Component({ selector: 'app-increment-decrement', templateUrl: 'increment-decrement.component.html', styleUrls: ['increment-decrement.component.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 IncrementDecrementComponent { num = signal(0); modify(n: number) { this.num.update((v) => (v += n)); }}
src/app/increment-decrement.component.html
<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>
src/app/increment-decrement.component.css
: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;}
Con CSS nativo
src/app/increment-decrement.component.ts
import {Component, ElementRef, OnInit, signal, viewChild} from '@angular/core';@Component({ selector: 'app-increment-decrement', templateUrl: 'increment-decrement.component.html', styleUrls: ['increment-decrement.component.css'],})export class IncrementDecrementComponent 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); }}
src/app/increment-decrement.component.html
<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>
src/app/increment-decrement.component.css
: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); }}
Animaciones padre / hijo
A diferencia del paquete de animaciones, cuando se especifican múltiples animaciones dentro de un componente dado, ninguna animación tiene prioridad sobre otra y nada bloquea que ninguna animación se dispare. Cualquier secuenciación de animaciones tendría que ser manejada por tu definición de tu animación CSS, usando retraso de animation / transition, y/o usando animationend o transitionend para manejar la adición del siguiente CSS a animar.
Deshabilitando una animación o todas las animaciones
Con animaciones CSS nativas, si deseas deshabilitar las animaciones que has especificado, tienes múltiples opciones.
- Crea una clase personalizada que fuerce la animación y transición a
none.
.no-animation { animation: none !important; transition: none !important;}
Aplicar esta clase a un elemento previene que cualquier animación se dispare en ese elemento. Alternativamente podrías aplicar esto a todo tu DOM o sección de tu DOM para forzar este comportamiento. Sin embargo, esto previene que los eventos de animación se disparen. Si estás esperando eventos de animación para la eliminación de elementos, esta solución no funcionará. Una solución alternativa es establecer las duraciones a 1 milisegundo en su lugar.
Usa la media query
prefers-reduced-motionpara asegurar que no se reproduzcan animaciones para usuarios que prefieren menos animación.Prevenir la adición de clases de animación programáticamente
Callbacks de animación
El paquete de animaciones exponía callbacks para que uses en el caso de que desees hacer algo cuando la animación haya terminado. Las animaciones CSS nativas también tienen estos callbacks.
OnAnimationStart
OnAnimationEnd
OnAnimationIteration
OnAnimationCancel
OnTransitionStart
OnTransitionRun
OnTransitionEnd
OnTransitionCancel
La API de Web Animations tiene mucha funcionalidad adicional. Echa un vistazo a la documentación para ver todas las APIs de animación disponibles.
NOTA: Ten en cuenta los problemas de propagación con estos callbacks. Si estás animando hijos y padres, los eventos se propagan desde los hijos hacia los padres. Considera detener la propagación o examinar más detalles dentro del evento para determinar si estás respondiendo al objetivo de evento deseado en lugar de un evento que se propaga desde un nodo hijo. Puedes examinar la propiedad animationname o las propiedades que están siendo transicionadas para verificar que tienes los nodos correctos.
Secuencias complejas
El paquete de animaciones tiene funcionalidad integrada para crear secuencias complejas. Estas secuencias son totalmente posibles sin el paquete de animaciones.
Dirigirse a elementos específicos
En el paquete de animaciones, podías dirigirte a elementos específicos usando la función query() para encontrar elementos específicos por un nombre de clase CSS, similar a document.querySelector(). Esto es innecesario en un mundo de animación CSS nativa. En su lugar, puedes usar tus selectores CSS para dirigirte a sub-clases y aplicar cualquier transform o animation deseada.
Para alternar clases para nodos hijos dentro de una plantilla, puedes usar enlaces de clase y estilo para agregar las animaciones en los puntos correctos.
Stagger()
La función stagger() te permitía retrasar la animación de cada elemento en una lista de elementos por un tiempo especificado para crear un efecto en cascada. Puedes replicar este comportamiento en CSS nativo utilizando animation-delay o transition-delay. Aquí hay un ejemplo de cómo podría verse ese CSS.
Con paquete de Animations
src/app/stagger.component.ts
import {Component, HostBinding, signal} from '@angular/core';import {trigger, transition, animate, style, query, stagger} from '@angular/animations';@Component({ selector: 'app-stagger', templateUrl: 'stagger.component.html', styleUrls: ['stagger.component.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 StaggerComponent { @HostBinding('@pageAnimations') items = [1, 2, 3];}
src/app/stagger.component.html
<h2>Stagger Example</h2><ul class="items"> @for(item of items; track item) { <li class="item">{{ item }}</li> }</ul>
src/app/stagger.component.css
.items { list-style: none; padding: 0; margin: 0;}
Con CSS nativo
src/app/stagger.component.ts
import {Component, signal} from '@angular/core';@Component({ selector: 'app-stagger', templateUrl: './stagger.component.html', styleUrls: ['stagger.component.css'],})export class StaggerComponent { show = signal(true); items = [1, 2, 3]; refresh() { this.show.set(false); setTimeout(() => { this.show.set(true); }, 10); }}
src/app/stagger.component.html
<h1>Stagger Example</h1><button type="button" (click)="refresh()">Refresh</button>@if (show()) { <ul class="items"> @for(item of items; track item) { <li class="item" style="--index: {{ item }}">{{item}}</li> } </ul>}
src/app/stagger.component.css
.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); }}
Animaciones paralelas
El paquete de animaciones tiene una función group() para reproducir múltiples animaciones al mismo tiempo. En CSS, tienes control total sobre el tiempo de animación. Si tienes múltiples animaciones definidas, puedes aplicarlas todas a la vez.
.target-element { animation: rotate 3s, fade-in 2s;}
En este ejemplo, las animaciones rotate y fade-in se disparan al mismo tiempo.
Animando los elementos de una lista que se reordena
El reordenamiento de elementos en una lista funciona de forma inmediata usando las técnicas descritas anteriormente. No se requiere ningún trabajo especial adicional. Los elementos en un bucle @for serán eliminados y re-agregados correctamente, lo que disparará animaciones usando @starting-styles para animaciones de entrada. Alternativamente, puedes usar animate.enter para este mismo comportamiento. Usa animate.leave para animar elementos a medida que se eliminan, como se ve en el ejemplo anterior.
Con paquete de Animations<
src/app/reorder.component.ts
import {Component, signal} from '@angular/core';import {trigger, transition, animate, query, style} from '@angular/animations';@Component({ selector: 'app-reorder', templateUrl: './reorder.component.html', styleUrls: ['reorder.component.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 ReorderComponent { 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; }}
src/app/reorder.component.html
<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>
src/app/reorder.component.css
.items { list-style: none; padding: 0; margin: 0;}
Con CSS nativo
src/app/reorder.component.ts
import {Component, signal} from '@angular/core';@Component({ selector: 'app-reorder', templateUrl: './reorder.component.html', styleUrls: ['reorder.component.css'],})export class ReorderComponent { 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; }}
src/app/reorder.component.html
<h1>Reordering List Example</h1><button type="button" (click)="randomize()">Randomize</button><ul class="items"> @for(item of items; track item) { <li class="item" animate.leave="fade">{{ item }}</li> }</ul>
src/app/reorder.component.css
.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; }}
Migrando usos de AnimationPlayer
La clase AnimationPlayer permite acceso a una animación para hacer cosas más avanzadas como pausar, reproducir, reiniciar y finalizar una animación a través de código. Todas estas cosas también se pueden manejar nativamente.
Puedes obtener animaciones de un elemento directamente usando Element.getAnimations(). Esto devuelve un array de cada Animation en ese elemento. Puedes usar la API de Animation para hacer mucho más de lo que podías con lo que ofrecía el AnimationPlayer del paquete de animaciones. Desde aquí puedes cancel(), play(), pause(), reverse() y mucho más. Esta API nativa debería proporcionar todo lo que necesitas para controlar tus animaciones.
Transiciones de ruta
Puedes usar transiciones de vista para animar entre rutas. Consulta la Guía de animaciones de transición de ruta para comenzar.