CSS ofrece un conjunto robusto de herramientas para crear animaciones hermosas y atractivas dentro de tu aplicación.
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
Puedes crear animaciones reutilizables que se pueden compartir en toda tu aplicación usando @keyframes. Define animaciones de keyframes en un archivo CSS compartido, y podrás reutilizar esas animaciones de keyframes donde quieras dentro de tu aplicación.
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
Es posible que desees animar entre dos estados diferentes, por ejemplo cuando un elemento está abierto o cerrado. Puedes lograr esto usando clases CSS ya sea mediante una animación de keyframe o estilo de transición.
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
Animar a menudo requiere ajustar comportamientos de tiempo, retrasos y easing. Esto se puede hacer 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
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. Aquí hay un ejemplo:
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
Animando altura automática
Puedes usar css-grid para animar a altura automática.
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
Puedes crear animaciones para cuando un elemento entra en una vista o sale de una vista. Comencemos viendo cómo animar un elemento que entra en una vista. Haremos esto con animate.enter, que aplicará clases de animación cuando un elemento entre en la vista.
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); }}
Animar un elemento cuando sale de la vista es similar a animar cuando entra en una vista. Usa animate.leave para especificar qué clases CSS aplicar cuando el elemento sale de la vista.
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
Animar en incremento y decremento es un patrón común en aplicaciones. Aquí hay un ejemplo de cómo puedes lograr ese comportamiento.
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); }}
Deshabilitando una animación o todas las animaciones
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
Si tienes acciones que te gustaría ejecutar en ciertos puntos durante las animaciones, hay varios eventos disponibles que puedes escuchar. Aquí hay algunos de ellos.
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
Las animaciones son a menudo más complicadas que solo un simple fade in o fade out. Puedes tener muchas secuencias complicadas de animaciones que desees ejecutar. Echemos un vistazo a algunos de esos posibles escenarios.
Escalonando animaciones en una lista
Un efecto común es escalonar las animaciones de cada elemento en una lista para crear un efecto en cascada. Esto se puede lograr utilizando animation-delay o transition-delay. Aquí hay un ejemplo de cómo podría verse ese CSS.
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
Puedes aplicar múltiples animaciones a un elemento a la vez usando la propiedad abreviada animation. Cada una puede tener sus propias duraciones y retrasos. Esto te permite componer animaciones juntas y crear efectos complicados.
.target-element { animation: rotate 3s, fade-in 2s;}
En este ejemplo, las animaciones rotate y fade-in se disparan al mismo tiempo, pero tienen diferentes duraciones.
Animando los elementos de una lista que se reordena
Los elementos en un bucle @for serán eliminados y re-agregados, 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 a continuación.
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; }}
Control programático de animaciones
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.
Más sobre animaciones de Angular
También puede que te interese lo siguiente: