Animaciones y Transiciones

Las animaciones y transiciones CSS permiten agregar movimiento y vida a una interfaz web sin JavaScript. Una transición anima el cambio suave de un valor CSS a otro (por ejemplo, un color que cambia al hacer hover) usando la propiedad transition. Una animación es más compleja: define fotogramas clave con @keyframes que describen cómo un elemento cambia a lo largo del tiempo, incluyendo múltiples etapas, repeticiones, direcciones alternadas y control sobre qué pasa antes y después de la animación. Ambas combinadas con transform (trasladar, rotar, escalar, sesgar) son la base de la UI moderna: micro-interacciones, feedback visual, transiciones de página y estados dinámicos.

La diferencia fundamental entre transiciones y animaciones es el trigger. Una transición se activa cuando una propiedad cambia (hover, clase agregada, state change) y se anima automáticamente desde el valor viejo al nuevo. Una animación se ejecuta de forma autónoma o cuando la aplicás con la propiedad animation, sin necesidad de un cambio de estado. Las transiciones son ideales para micro-interacciones sutiles (hover effects, toggle states), mientras que las animaciones son mejores para efectos complejos (entradas de elementos, loaders, ciclos continuos). La regla general: si puede ser una transición, usá una transición—es más simple y más predecible.

Transiciones CSS

Las transiciones CSS permiten animar el cambio de una propiedad entre dos valores. Cuando una propiedad CSS cambia (por un hover, un cambio de clase via JS, un pseudo-elemento, etc.), la transición interpola los valores intermedios durante la duración especificada en lugar de cambiar instantáneamente. La propiedad transition es un shorthand que combina hasta cuatro sub-propiedades: transition-property (qué propiedades animar), transition-duration (cuánto dura), transition-timing-function (cómo se acelera/desacelera) y transition-delay (cuánto esperar antes de empezar).

Experimentá con las transiciones en vivo. Elegí qué propiedad animar, la duración, la curva de easing y el delay. Probá los dos modos: hover (pasá el mouse sobre el elemento) y click (toggle con clic). Abajo podrás comparar visualmente cómo se mueven las diferentes curvas de easing.

Playground — Transiciones CSS
CSS
/* ============================================
   TRANSICIONES CSS: sintaxis completa
   ============================================ */

/* Sintaxis shorthand (la más usada) */
.btn {
    /* transition: property duration timing-function delay */
    transition: background-color 0.3s ease, transform 0.2s ease;
}

/* Sintaxis expandida (más legible cuando hay muchas) */
.btn {
    transition-property: background-color, transform, box-shadow;
    transition-duration: 0.3s, 0.2s, 0.3s;
    transition-timing-function: ease;
    transition-delay: 0s;
}

/* ============================================
   TRANSICION COMÚN: hover effects
   ============================================ */

.btn {
    background-color: #3b82f6;
    color: white;
    padding: 0.75rem 1.5rem;
    border-radius: 0.5rem;
    transition: all 0.3s ease;
    cursor: pointer;
}

.btn:hover {
    background-color: #2563eb;
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
}

.btn:active {
    transform: translateY(0);
    box-shadow: 0 1px 4px rgba(59, 130, 246, 0.2);
}

/* ============================================
   TRANSICIONES con cambio de clase (JS)
   ============================================ */

/* Elemento con estado "abierto" / "cerrado" */
.panel {
    max-height: 0;
    overflow: hidden;
    transition: max-height 0.5s ease, padding 0.3s ease;
    padding: 0 1rem;
}

.panel.open {
    max-height: 500px;    /* Valor suficiente para el contenido */
    padding: 1rem;
}

/* ============================================
   TRANSICIONES: múltiples propiedades
   con distintas duraciones
   ============================================ */

.card {
    background: var(--surface);
    border-radius: 1rem;
    padding: 1.5rem;
    /* Cada propiedad tiene su propia duracion */
    transition:
        transform 0.2s ease,
        box-shadow 0.3s ease,
        background-color 0.5s ease;
}

.card:hover {
    transform: translateY(-4px);
    box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
    background-color: var(--surface-hover);
}

/* ============================================
   TRANSICIONES: shorthand con valores mixtos
   ============================================ */

.link {
    color: var(--accent-blue);
    text-decoration: underline;
    text-underline-offset: 3px;
    /* Transicion simple de una propiedad */
    transition: color 0.2s ease;
}

.link:hover {
    color: var(--accent-cyan);
}
Sub-propiedad Valores Default Descripción
transition-property Nombre de propiedad CSS, all, none all Qué propiedades animar. all transiciona todo (cuidado: puede ser costoso).
transition-duration Tiempo: 0.3s, 300ms, 0s 0s Duración de la animación. Sin duración, no hay transición (cambio instantáneo).
transition-timing-function ease, linear, ease-in, ease-out, ease-in-out, cubic-bezier() ease Curva de aceleración. Define cómo se distribuye la velocidad durante la transición.
transition-delay Tiempo: 0.1s, 200ms 0s Tiempo de espera antes de iniciar. Se aplica tanto al entrar como al salir.

No toda propiedad es animable

Solo se pueden transicionar propiedades que tienen valores numéricos o de color: color, background-color, width, height, padding, margin, opacity, transform, box-shadow, border-radius, border-color, font-size, line-height. Propiedades como display (block/none), height: auto, visibility: hidden y z-index NO se transicionan directamente. Para ocultar/mostrar con transición, usá opacity + pointer-events o max-height como workaround.

Animaciones con @keyframes

Las animaciones con @keyframes son más potentes que las transiciones porque definen múltiples puntos de control (fotogramas clave) en los que el elemento cambia de estado. Mientras una transición solo puede ir del punto A al punto B, una animación puede ir de A a B a C a D, con diferentes estados intermedios. Se definen con @keyframes nombre { ... } y se aplican con la propiedad animation. Los keyframes pueden usar from/to (equivalente a 0%/100%) o porcentajes arbitrarios para definir estados intermedios.

CSS
/* ============================================
   @KEYFRAMES: sintaxis básica
   ============================================ */

/* Con from/to (solo inicio y fin) */
@keyframes fade-in {
    from {
        opacity: 0;
    }
    to {
        opacity: 1;
    }
}

/* Con porcentajes (múltiples puntos de control) */
@keyframes slide-up {
    0% {
        opacity: 0;
        transform: translateY(30px);
    }
    50% {
        opacity: 0.7;
        transform: translateY(5px);
    }
    100% {
        opacity: 1;
        transform: translateY(0);
    }
}

/* ============================================
   APLICAR la animación con la propiedad animation
   ============================================ */

.elemento {
    /* animation: name duration timing-function delay
                  iteration-count direction fill-mode play-state; */
    animation: fade-in 0.6s ease-out forwards;
}

/* Sintaxis expandida (más legible) */
.elemento {
    animation-name: slide-up;
    animation-duration: 0.6s;
    animation-timing-function: ease-out;
    animation-delay: 0.2s;
    animation-iteration-count: 1;
    animation-direction: normal;
    animation-fill-mode: forwards;
    animation-play-state: running;
}

/* ============================================
   EJEMPLOS PRACTICOS de @keyframes
   ============================================ */

/* --- Fade in + slide up (muy usado para on-scroll) --- */
@keyframes fade-slide-in {
    from {
        opacity: 0;
        transform: translateY(30px);
    }
    to {
        opacity: 1;
        transform: translateY(0);
    }
}

.card {
    opacity: 0;              /* Estado inicial invisible */
    animation: fade-slide-in 0.6s ease-out forwards;
}

/* --- Stagger: cards que entran una detrás de otra --- */
.card:nth-child(1) { animation-delay: 0s; }
.card:nth-child(2) { animation-delay: 0.1s; }
.card:nth-child(3) { animation-delay: 0.2s; }
.card:nth-child(4) { animation-delay: 0.3s; }

/* --- Pulse (feedback visual para notificaciones) --- */
@keyframes pulse {
    0%, 100% {
        transform: scale(1);
        box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.4);
    }
    50% {
        transform: scale(1.05);
        box-shadow: 0 0 0 8px rgba(59, 130, 246, 0);
    }
}

.notification {
    animation: pulse 2s infinite;
}

/* --- Spin (loader) --- */
@keyframes spin {
    to { transform: rotate(360deg); }
}

.spinner {
    width: 40px;
    height: 40px;
    border: 3px solid var(--border-color);
    border-top-color: var(--accent-blue);
    border-radius: 50%;
    animation: spin 0.8s linear infinite;
}

/* --- Bounce --- */
@keyframes bounce {
    0%, 100% {
        transform: translateY(0);
        animation-timing-function: ease-out;
    }
    50% {
        transform: translateY(-20px);
        animation-timing-function: ease-in;
    }
}

.bounce-icon {
    animation: bounce 1s infinite;
}

/* --- Typing effect para el cursor --- */
@keyframes blink-cursor {
    0%, 100% { opacity: 1; }
    50% { opacity: 0; }
}

.cursor::after {
    content: '|';
    animation: blink-cursor 1s step-end infinite;
}

/* --- Shake (error de formulario) --- */
@keyframes shake {
    0%, 100% { transform: translateX(0); }
    10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); }
    20%, 40%, 60%, 80% { transform: translateX(4px); }
}

.input-error {
    animation: shake 0.5s ease-in-out;
    border-color: var(--accent-red);
}

Propiedades de animation

La propiedad animation controla cómo se ejecuta una animación definida con @keyframes. Tiene ocho sub-propiedades que determinan el nombre, la duración, la curva de velocidad, el retraso, las repeticiones, la dirección, el fill mode (qué pasa antes y después) y el play state (pausada o reproduciéndose). Combinar estas propiedades da control total sobre el comportamiento de la animación.

Sub-propiedad Valores comunes Default Descripción
animation-name Nombre del @keyframes none Qué animación ejecutar.
animation-duration 0.5s, 1s, 500ms 0s Cuánto dura una iteración.
animation-timing-function ease, linear, cubic-bezier() ease Curva de aceleración (igual que en transiciones).
animation-delay 0.2s, 200ms 0s Tiempo de espera antes de iniciar.
animation-iteration-count Número, infinite 1 Cuántas veces se repite. infinite para loops.
animation-direction normal, reverse, alternate, alternate-reverse normal alternate va ida y vuelta (ideal para loops suaves).
animation-fill-mode none, forwards, backwards, both none forwards: mantiene el estado final. backwards: aplica el estado inicial antes de iniciar. both: ambos.
animation-play-state running, paused running Para/reanuda la animación. útil para pausar en hover.
CSS
/* ============================================
   ANIMATION-FILL-MODE: qué pasa antes/después
   ============================================ */

/* forwards: mantiene el ESTADO FINAL de la animación */
.fade-element {
    opacity: 0;
    animation: fade-in 1s ease forwards;
    /* Después de la animación, opacity queda en 1
       (no vuelve al valor original de 0) */
}

/* backwards: aplica el ESTADO INICIAL antes del delay */
.fade-element-delay {
    opacity: 0;
    animation: fade-in 1s ease 0.5s backwards;
    /* Durante los 0.5s de delay, el elemento tiene
       los estilos del 0% del keyframe (opacity: 0)
       en lugar de su valor por defecto */
}

/* both: combina forwards + backwards */
.fade-element-both {
    opacity: 0;
    animation: fade-in 1s ease 0.5s both;
}

/* ============================================
   ANIMATION-DIRECTION: alternate para loops
   ============================================ */

@keyframes float {
    0%   { transform: translateY(0); }
    100% { transform: translateY(-10px); }
}

.floating-icon {
    animation: float 3s ease-in-out infinite alternate;
    /* Va de arriba a abajo y viceversa suavemente.
       alternate + infinite = ida y vuelta infinito */
}

/* ============================================
   ANIMATION-PLAY-STATE: pausar en hover
   ============================================ */

.marquee {
    animation: scroll-left 10s linear infinite;
}

.marquee:hover {
    animation-play-state: paused;
}

/* ============================================
   MÚLTIPLES ANIMACIONES en un solo elemento
   ============================================ */

.card-enter {
    animation:
        fade-in 0.5s ease forwards,
        slide-up 0.5s ease forwards;
}

/* Con delays diferentes para stagger */
.card-enter:nth-child(1) { animation-delay: 0s; }
.card-enter:nth-child(2) { animation-delay: 0.1s, 0.1s; }
.card-enter:nth-child(3) { animation-delay: 0.2s, 0.2s; }

/* ============================================
   ANIMACIONES EN cascade con custom properties
   ============================================ */

/* Cada card calcula su delay automáticamente */
.card {
    --index: 0;
    animation: fade-slide-in 0.5s ease both;
    animation-delay: calc(var(--index) * 0.1s);
}

.card:nth-child(1) { --index: 0; }
.card:nth-child(2) { --index: 1; }
.card:nth-child(3) { --index: 2; }
.card:nth-child(4) { --index: 3; }

Transform: translate, rotate, scale, skew

transform aplica transformaciones geométricas a un elemento: traslación (mover), rotación (girar), escala (agrandar/achicar) y sesgado (deformar). La clave de transform es que no afecta el layout—el elemento se mueve visualmente pero no altera el flujo del documento ni el tamaío que ocupa. Esto es lo que lo hace tan eficiente para animaciones: el navegador puede procesar transformaciones sin recalcular el layout (reflow), solo necesita repintar (repaint) la posición del elemento. Es la diferencia entre 60fps suave y animaciones que tartea el navegador.

Funciones de transformación

CSS
/* ============================================
   TRANSFORM: funciones principales
   ============================================ */

/* --- TRANSLATE: mover el elemento --- */
.move-right  { transform: translateX(50px); }    /* 50px a la derecha */
.move-up     { transform: translateY(-20px); }   /* 20px hacia arriba */
.move-both   { transform: translate(50px, -20px); } /* X e Y */
.move-center { transform: translate(-50%, -50%); } /* Centrar con position: absolute */

/* translate() vs top/left:
   translate NO causa reflow (más rápido)
   top/left SÍ causa reflow (más lento)
   SIEMPRE preferí translate para animaciones */

/* --- ROTATE: girar el elemento --- */
.rotate-45  { transform: rotate(45deg); }
.rotate-90  { transform: rotate(0.5turn); }  /* Media vuelta */
.rotate-neg { transform: rotate(-15deg); }  /* 15 grados antihorario */
.rotate-x   { transform: rotateX(45deg); }  /* Rotar en eje X (3D) */
.rotate-y   { transform: rotateY(180deg); } /* Flip horizontal (3D) */

/* --- SCALE: escalar el elemento --- */
.scale-up   { transform: scale(1.2); }   /* 20% más grande */
.scale-down { transform: scale(0.8); }   /* 20% más chico */
.scale-x    { transform: scaleX(2); }    /* Solo ancho */
.scale-y    { transform: scaleY(0.5); }  /* Solo alto */

/* --- SKEW: sesgar el elemento --- */
.skew-x     { transform: skewX(15deg); }
.skew-y     { transform: skewY(-10deg); }
.skew-both  { transform: skew(15deg, -10deg); }

/* ============================================
   TRANSFORM: combinar funciones
   ============================================ */

/* El orden importa! Se aplican de derecha a izquierda */
.card:hover {
    transform: translateY(-4px) scale(1.02);
    /* Primero escala, luego mueve */
}

.btn:active {
    transform: scale(0.95);
    /* Efecto de "apretar" el botón */
}

/* 3D perspective flip */
.card-3d {
    perspective: 1000px; /* Se aplica al PADRE */
}

.card-3d-inner {
    transition: transform 0.6s;
    transform-style: preserve-3d; /* Los hijos mantienen 3D */
}

.card-3d:hover .card-3d-inner {
    transform: rotateY(180deg);
}

/* ============================================
   TRANSFORM-ORIGIN: punto de origen
   ============================================ */

/* Por defecto, el origen es el centro del elemento (50% 50%) */
.rotate-corner {
    transform-origin: top left; /* Rota desde la esquina superior izquierda */
    transform: rotate(45deg);
}

.scale-bottom {
    transform-origin: bottom center; /* Escala desde abajo */
    transform: scaleX(0);
}

/* ============================================
   TRANSFORM: patrones comunes animados
   ============================================ */

/* --- Hover lift (el más usado en la web) --- */
.hover-lift {
    transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.hover-lift:hover {
    transform: translateY(-2px);
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}

/* --- Expand on click --- */
.accordion-content {
    transform: scaleY(0);
    transform-origin: top;
    transition: transform 0.3s ease;
}

.accordion-content.open {
    transform: scaleY(1);
}

/* --- Icon rotation on hover --- */
.icon-spin:hover {
    transform: rotate(90deg);
    transition: transform 0.3s ease;
}

/* --- Tilt effect (3D en hover) --- */
.tilt-card {
    transition: transform 0.3s ease;
}

.tilt-card:hover {
    transform: perspective(600px) rotateY(-5deg) rotateX(5deg);
}
Función Qué hace Ejemplo Uso típico
translate(tx, ty) Mueve el elemento en X e Y translate(10px, -20px) Hover lift, slide-in, tooltips, centering
rotate(angulo) Gira el elemento rotate(45deg) Iconos, loaders, flip cards
scale(sx, sy) Escala el ancho y alto scale(1.1) Botón active, zoom, focus ring
skew(ax, ay) Sesga (deforma) el elemento skew(-10deg) Efectos decorativos, distorsión
matrix(a,b,c,d,tx,ty) Transformación completa en una matriz Complejo, rara vez se usa directamente Transformaciones calculadas con JS

translate vs top/left para animaciones

translate se procesa en la etapa de composite (la más rápida del pipeline de renderizado), mientras que top/left dispara un reflow (recálculo del layout completo). Para animaciones, SIEMPRE usá transform: translate() en lugar de top/left. La diferencia de rendimiento puede ser de 60fps a 15fps en páginas complejas.

Easing Functions: la curva de movimiento

La easing function (o curva de tiempo) define cómo se distribuye la velocidad durante una animación o transición. Una curva linear tiene velocidad constante (robotica, poco natural). Una curva ease arranca lento, acelera en el medio y desacelera al final (suave, natural). La elección de la curva tiene un impacto enorme en cómo se siente una animación—la misma duración con distintas curvas produce sensaciones completamente diferentes. Las curvas más usadas son ease, ease-out, ease-in-out y cubic-bezier() para curvas totalmente custom.

CSS
/* ============================================
   EASING FUNCTIONS: curvas de movimiento
   ============================================ */

/* --- Keywords predefinidos --- */

/* linear: velocidad constante (sin aceleracion/desaceleracion)
   Usar para: spinners, barras de progreso, marquee */
animation: spin 1s linear infinite;

/* ease: arranca lento, acelera, desacelera al final
   Es el default para animation y transition.
   Usar para: animaciones generales */
transition: opacity 0.3s ease;

/* ease-in: arranca lento, termina rápido
   Usar para: elementos que salen (fade-out, slide-out) */
transition: opacity 0.3s ease-in;

/* ease-out: arranca rápido, termina lento
   Usar para: elementos que entran (fade-in, slide-in, hover)
   RECOMENDADO para la mayoría de interacciones */
transition: transform 0.2s ease-out;

/* ease-in-out: lento al inicio y al final, rápido en el medio
   Usar para: toggle states, elementos que van y vienen */
transition: max-height 0.5s ease-in-out;

/* ============================================
   CUBIC-BEZIER: curvas totalmente custom
   ============================================ */
/* cubic-bezier(x1, y1, x2, y2)
   - x1, x2: puntos de control en el eje X (tiempo, 0 a 1)
   - y1, y2: puntos de control en el eje Y (progreso, puede ser negativo)
   Los valores x DEBEN estar entre 0 y 1 */

/* Material Design standard (muy popular) */
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);

/* Material Design decelerate (para elementos que entran) */
transition: transform 0.3s cubic-bezier(0, 0, 0.2, 1);

/* Material Design accelerate (para elementos que salen) */
transition: transform 0.3s cubic-bezier(0.4, 0, 1, 1);

/* Bounce-like (rebote suave sin @keyframes) */
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);

/* Snap (empieza lento, da un "salto" al final) */
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);

/* Smooth ease-out extra suave */
transition: opacity 0.5s cubic-bezier(0.22, 1, 0.36, 1);

/* ============================================
   STEPS(): pasos discretos (no interpolacion)
   ============================================ */
/* steps(numero, jump-start | jump-end | jump-none | jump-both)
   Divide la animación en N pasos discretos,
   sin interpolación entre ellos */

/* Blinking cursor: 2 pasos, en cada paso cambia de estado */
@keyframes blink {
    0%, 100% { opacity: 1; }
    50% { opacity: 0; }
}

.cursor {
    animation: blink 1s step-end infinite;
    /* step-end = jump-end: aplica cada paso AL FINAL de cada intervalo */
}

/* Sprite sheet animation: 8 frames */
@keyframes sprite-walk {
    from { background-position: 0; }
    to   { background-position: -800px; } /* 8 frames * 100px */
}

.character {
    width: 100px;
    height: 100px;
    background: url('sprite-sheet.png');
    animation: sprite-walk 0.8s steps(8) infinite;
    /* Cada frame dura 0.1s (0.8s / 8 steps) */
}

/* ============================================
   EASING recomendaciones prácticas
   ============================================ */

/* Hover effects: ease-out (entrada rápida, salida suave) */
.btn {
    transition: background-color 0.2s ease-out,
                transform 0.15s ease-out;
}

/* Modals / Overlays: cubic-bezier suave */
.modal-overlay {
    transition: opacity 0.3s cubic-bezier(0.22, 1, 0.36, 1);
}

/* Layout changes: ease-in-out (symétrico) */
.sidebar {
    transition: width 0.3s ease-in-out;
}

/* Loading / spinners: linear (velocidad constante) */
.loader {
    animation: spin 1s linear infinite;
}
Easing Sensación Mejor para Cubic-bezier equivalente
linear Constante, mecánico Spinners, progress bars cubic-bezier(0, 0, 1, 1)
ease Suave general Default, animaciones genéricas cubic-bezier(0.25, 0.1, 0.25, 1)
ease-in Arranca lento, termina rápido Elementos que salen (fade-out) cubic-bezier(0.42, 0, 1, 1)
ease-out Arranca rápido, termina lento Elementos que entran, hovers, focus cubic-bezier(0, 0, 0.58, 1)
ease-in-out Suave en ambos extremos Toggle states, loops, layout changes cubic-bezier(0.42, 0, 0.58, 1)
steps(n) Discreto, sin interpolación Sprites, cursor blink, typewriter No aplica (función diferente)

Performance: 60fps sin tirones

La performance de las animaciones CSS depende de entender el pixel pipeline del navegador. Cuando animás una propiedad, el navegador pasa por hasta cinco etapas: JavaScript (trigger), Style (calcular estilos), Layout (calcular posición y tamaño), Paint (rellenar píxeles) y Composite (combinar capas en pantalla). Cuantas menos etapas atraviese tu animación, más rápida será. Las animaciones de transform y opacity solo pasan por Composite—son las más rápidas. Las animaciones de width, height, top, left o margin atraviesan Layout + Paint + Composite—son las más lentas y pueden causar tirones (jank) en dispositivos móviles.

CSS
/* ============================================
   PIXEL PIPELINE: del trigger al frame
   ============================================ */
/*
   JS  →  Style  →  Layout  →  Paint  →  Composite
                          (reflow)  (repaint) (compositing)

   Las propiedades más rápidas para animar son:
   1. transform  (solo Composite)     <<< MEJOR
   2. opacity    (solo Composite)     <<< MEJOR

   Propiedades que causan Layout (EVITAR animar):
   - width, height
   - top, right, bottom, left
   - margin, padding
   - font-size, line-height

   Propiedades que causan Paint (aceptable si no hay Layout):
   - color, background-color
   - box-shadow
   - border-radius, border-color
*/

/* ============================================
   MAL vs BIEN: propiedades de animación
   ============================================ */

/* MAL: animar width/height (causa Layout en cada frame) */
.expand-bad {
    width: 100px;
    transition: width 0.3s ease;
}
.expand-bad:hover {
    width: 300px; /* Layout thrashing! */
}

/* BIEN: animar transform (solo Composite) */
.expand-good {
    transform: scaleX(0.33); /* 100/300 = 0.33 */
    transform-origin: left;
    transition: transform 0.3s ease;
}
.expand-good:hover {
    transform: scaleX(1);
}

/* MAL: animar top/left para mover elementos */
.move-bad {
    position: relative;
    top: 0;
    transition: top 0.3s ease;
}
.move-bad:hover {
    top: -20px; /* Layout en cada frame! */
}

/* BIEN: usar transform: translate */
.move-good {
    transition: transform 0.3s ease;
}
.move-good:hover {
    transform: translateY(-20px); /* Solo Composite! */
}

/* ============================================
   WILL-CHANGE: hint para el navegador
   ============================================ */

/* Le dice al navegador que esta propiedad va a cambiar,
   para que haga optimizaciones anticipadas (crear capa GPU).
   USAR CON MODERACION: no lo pongas en todo. */

/* Buen uso: en un elemento que sí va a animarse */
.animated-card {
    will-change: transform, opacity;
}

/* Bad: no lo pongas en todo por defecto */
/* * { will-change: transform; }  -- MAL!
   Crea una capa GPU por cada elemento,
   consume memoria y puede EMPEORAR la performance */

/* ============================================
   CONTAINMENT: aislar repaints
   ============================================ */

/* contain: aísla los efectos de renderizado de un elemento
   Si un hijo cambia, no fuerza repaint de elementos fuera
   del contenedor */

.card-container {
    contain: layout style paint;
    /* layout: el layout del container no afecta afuera
       style: los estilos no se filtran hacia afuera
       paint: los cambios visuales se limitan al container */
}

/* ============================================
   LAYER PROMOTION para animaciones complejas
   ============================================ */

/* Forzar una capa GPU con translateZ o will-change */
.gpu-layer {
    /* Opcion 1: will-change (recomendado) */
    will-change: transform;

    /* Opcion 2: translateZ hack (legacy) */
    /* transform: translateZ(0); */

    /* Opcion 3: translate3d hack (legacy) */
    /* transform: translate3d(0, 0, 0); */
}

/* ============================================
   PROMESAS DE PERFORMANCE para animaciones
   ============================================ */

/* 1. Solo anima transform y opacity */
.card {
    transition: transform 0.2s ease, opacity 0.3s ease;
}

/* 2. Usa will-change solo en lo que va a animarse */
.hero-image {
    will-change: transform;
    transition: transform 0.5s cubic-bezier(0.22, 1, 0.36, 1);
}

/* 3. Respeta prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
    }
}

/* 4. Remueve will-change después de la animacion */
/* En JS:
   element.style.willChange = 'transform';
   element.addEventListener('transitionend', () => {
       element.style.willChange = 'auto';
   });
*/

will-change: úsalo con moderación

will-change no es una solución mágica para malas animaciones. Si lo aplicás a muchos elementos, el navegador creará una capa GPU por cada uno, consumiendo RAM de video y potencialmente empeorando la performance. Solo usálo en los elementos que realmente van a animarse (como un hero image que se mueve con parallax), y remuévelo después de la animación con JavaScript. No lo apliques en *, no lo pongas en CSS base, no lo uses como "seguro".

View Transitions API: transiciones entre páginas

La View Transitions API es una API relativamente nueva (Chrome 111+, Edge 111+, Opera 97+) que permite crear transiciones animadas entre navegaciones de páginas (MPA) o entre vistas dentro de una SPA. Antes de esta API, las transiciones entre páginas solo eran posibles en Single Page Applications (React/Vue) donde controlás el DOM. Con View Transitions, incluso un sitio multi-página tradicional (como WebForge) puede tener transiciones suaves al cambiar de página: un fade, un slide, o un morphing del contenido viejo al nuevo. La API captura automáticamente un snapshot del estado viejo, espera el DOM nuevo, captura un snapshot del estado nuevo, y anima la transición entre ambos.

JS
// ============================================
// View Transitions API: transiciones entre paginas
// ============================================

// --- MPA (Multi-Page App): la forma más simple ---
// En cada página donde querés transición al navegar:

// Detectar un link click y hacer la transición
document.querySelectorAll('a').forEach(link => {
    link.addEventListener('click', async (e) => {
        e.preventDefault();
        const href = link.href;

        // Si el navegador soporta View Transitions
        if (document.startViewTransition) {
            document.startViewTransition(() => {
                // Actualizar el DOM (navegar)
                document.location.href = href;
            });
        } else {
            // Fallback: navegación normal
            document.location.href = href;
        }
    });
});

// --- CSS: animar la transición ---
// El navegador crea pseudo-elementos automáticamente:
// ::view-transition-old(root) = snapshot VIEJO
// ::view-transition-new(root) = snapshot NUEVO

/*
::view-transition-old(root) {
    animation: fade-out 0.3s ease-out;
}

::view-transition-new(root) {
    animation: fade-in 0.3s ease-out;
}

@keyframes fade-out {
    to { opacity: 0; }
}

@keyframes fade-in {
    from { opacity: 0; }
}
*/

// --- Transición de slide (página vieja sale, nueva entra) ---
/*
::view-transition-old(root) {
    animation: slide-out 0.3s ease-in;
}

::view-transition-new(root) {
    animation: slide-in 0.3s ease-out;
}

@keyframes slide-out {
    to { transform: translateX(-100%); opacity: 0; }
}

@keyframes slide-in {
    from { transform: translateX(100%); opacity: 0; }
}
*/

// --- Transición NAMED: animar elementos específicos ---
// HTML viejo:
// <img src="foto.jpg" view-transition-name="hero-img">
// HTML nuevo:
// <img src="foto-otra.jpg" view-transition-name="hero-img">

// CSS: el navegador detecta que ambos tienen el mismo
// view-transition-name y hace un morphing entre ellos
/*
::view-transition-old(hero-img) {
    animation: scale-down 0.3s ease;
}

::view-transition-new(hero-img) {
    animation: scale-up 0.3s ease;
}
*/

Soporte de navegadores

La View Transitions API tiene soporte en Chrome/Edge 111+, Opera 97+, y Safari 18+. Firefox aún no la soporta nativamente (en behind a flag). Para producción, es esencial usar feature detection con if (document.startViewTransition) y proporcionar un fallback sin animación (navegación normal). A medida que los navegadores van adoptando la API, la experiencia mejora progresivamente. Es una feature relativamente nueva pero con un futuro prometedor: elimina la necesidad de frameworks SPA solo para tener transiciones entre páginas.

Navegador Soporte Notas
Chrome / Edge 111+ (marzo 2023) Soporte completo con named transitions.
Firefox Behind flag En desarrollo activo. No disponible por defecto.
Safari 18+ (2024) Soporte completo, incluido iOS Safari 18+.
Feature Detection document.startViewTransition Si es undefined, el navegador no lo soporta. Fallback a navegación normal.

Progressive Enhancement

La View Transitions API es un ejemplo perfecto de progressive enhancement: en navegadores que la soportan, el usuario ve una transición suave. En los que no, la navegación funciona normalmente sin animación. No hay breakage, no hay error—solo una experiencia ligeramente diferente. Este es el enfoque correcto para features modernas en la web.

Buenas prácticas

Las animaciones pueden hacer o deshacer una interfaz. Bien usadas, mejoran la usabilidad: dan feedback inmediato, guían la atención, comunican relaciones espaciales y hacen que la interfaz se sienta responsiva. Mal usadas, distraen, frustan, causan mareos (motion sickness) y empeoran la accesibilidad. Las siguientes reglas te ayudan a encontrar el balance correcto entre "vivo" y "funcional".

CSS
/* ============================================
   BUENAS PRACTICAS PARA ANIMACIONES
   ============================================ */

/* --- 1. RESPETA prefers-reduced-motion --- */
/* OBLIGATORIO. Si el usuario prefiere menos movimiento,
   desactivá todas las animaciones. */
@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
    }
}

/* También podés definir una custom property
   y usarla en tus animaciones */
:root {
    --animation-duration: 0.6s;
    --transition-duration: 0.3s;
}

@media (prefers-reduced-motion: reduce) {
    :root {
        --animation-duration: 0.01ms;
        --transition-duration: 0.01ms;
    }
}

.card {
    animation: fade-in var(--animation-duration) ease forwards;
}

/* --- 2. Duraciones cortas y consistentes --- */
/* Las micro-interacciones: 150-300ms
   Las transiciones de contenido: 300-500ms
   Las animaciones de entrada: 500-800ms
   Nada debería durar más de 1s */

/* Duraciones recomendadas (en Custom Properties) */
:root {
    --duration-instant: 100ms;   /* Click feedback */
    --duration-fast:    150ms;   /* Hover, focus */
    --duration-normal:  300ms;   /* Transiciones generales */
    --duration-slow:    500ms;   /* Modales, page transitions */
    --duration-slower:  800ms;   /* Animaciones de entrada */
}

/* --- 3. Usa ease-out para entradas, ease-in para salidas --- */
.element-enter {
    animation: fade-in 0.3s ease-out;
}

.element-exit {
    animation: fade-out 0.3s ease-in;
}

/* --- 4. Evita animar propiedades que causan reflow --- */
/* BIEN: transform + opacity */
.btn {
    transition: transform 0.15s ease-out, opacity 0.2s ease;
}
.btn:hover {
    transform: translateY(-1px);
}

/* MAL: width, height, margin, padding, top, left */
/* Estas fuerzan Layout en cada frame del navegador */

/* --- 5. No hagas animaciones redundantes --- */
/* Si el usuario hizo click en algo, NO necesitas
   una animación de 3 segundos. El feedback debe
   ser inmediato y corto (100-300ms) */

/* --- 6. Evita loops infinitos visibles --- */
/* Los loops infinitos son aceptables SOLO para:
   - Spinners / loaders (mientras carga algo)
   - Indicadores sutiles (cursor blink)
   - Elementos periféricos (no el contenido principal)

   MAL: un hero text con typewriter loop infinito
   BIEN: un spinner mientras carga un formulario */

/* --- 7. Usa animation-fill-mode: forwards --- */
/* Para animaciones de entrada que deberían
   mantener su estado final */
.fade-in {
    opacity: 0;
    animation: fade-in 0.5s ease forwards;
    /* forwards: mantiene opacity: 1 al finalizar */
}
Regla Por qué Ejemplo
Respeta prefers-reduced-motion Algunos usuarios tienen vestibulares o mareos. Es obligatorio por WCAG. Desactivar todas las animaciones cuando el usuario lo prefiere.
Duraciones cortas Animaciones largas frustan al usuario. 150-500ms cubre el 99% de los casos. Hover: 150ms, transiciones: 300ms, entrada: 500ms.
Solo anima transform y opacity Son las únicas propiedades que no causan reflow. 60fps garantizado. Usa translate en lugar de top, scale en lugar de width.
Easing natural La curva define la "personalidad" de la animación. ease-out es la más natural. Entradas: ease-out, salidas: ease-in, toggle: ease-in-out.
Propósito funcional Toda animación debe comunicar algo, no solo decorar. Hover lift = interactivo, fade-in = aparece contenido, shake = error.
will-change moderado Solo en elementos que realmente se animan. Remover después. Aplicar en hover, remover al final de la transición.