Animar entre dos layouts que CSS no puede interpolar
Por qué no se puede hacer una transición de grid a flex, de una columna a dos, o de una celda a otra, y cómo Flip resuelve cada uno de esos casos.
La pregunta que lleva a Flip suele formularse así: por qué no puedo hacer una transición entre display: grid y display: flex. La respuesta corta es que display no es interpolable, pero la respuesta corta no ayuda porque el problema es mucho más general y no se limita a esa propiedad. La cuestión de fondo es que el layout no es un valor sino el resultado de un algoritmo, y no existe un camino continuo entre el resultado de dos algoritmos distintos. Flip evita el problema entero al no intentar interpolar nada del layout.
- Explicar por qué el layout no es interpolable y qué propiedades entran en esa categoría.
- Resolver la transición entre dos disposiciones distintas del mismo contenedor.
- Animar el paso de una vista de lista a una de cuadrícula.
- Manejar el caso de los objetivos anidados con
nested.
Qué significa “no interpolable”
Una transición de CSS necesita poder calcular estados intermedios. Con opacity de 0 a 1 el punto medio es 0,5, y eso está definido. Con width de 100px a 300px el punto medio es 200px. Con display de grid a flex no hay punto medio: no existe un valor a mitad de camino entre dos algoritmos de disposición.
La lista de propiedades que causan este problema es más larga de lo que parece: display, position, float, grid-template-areas, flex-direction, order, align-items, y cualquier cambio en la estructura del DOM, que ni siquiera es una propiedad. Todas comparten la misma naturaleza: no describen una geometría, describen cómo se calcula la geometría.
Y hay un caso todavía más frecuente que ninguno de esos: mover un nodo de un contenedor a otro. Ahí no cambia ninguna propiedad; cambia el árbol.
Flip funciona precisamente porque no intenta interpolar nada de eso. Deja que el navegador calcule el layout final con el algoritmo que sea, mide el resultado, y anima la diferencia geométrica con transformaciones. Le da igual si el cambio fue de grid a flex, de un contenedor a otro o de un orden a otro: para Flip todo se reduce a que un rectángulo estaba aquí y ahora está allí.
Tres casos resueltos
Caso uno: cambiar la disposición del contenedor
El ejemplo canónico. Un contenedor que alterna entre una cuadrícula de tres columnas y una lista vertical.
<button id="cambiar">Cambiar vista</button>
<div class="contenedor cuadricula">
<article class="tarjeta">Uno</article>
<article class="tarjeta">Dos</article>
<article class="tarjeta">Tres</article>
<article class="tarjeta">Cuatro</article>
<article class="tarjeta">Cinco</article>
<article class="tarjeta">Seis</article>
</div>
.contenedor { gap: 1rem; }
.contenedor.cuadricula { display: grid; grid-template-columns: repeat(3, 1fr); }
.contenedor.lista { display: flex; flex-direction: column; }
.tarjeta { box-sizing: border-box; padding: 1rem; background: #313244; color: #cdd6f4; }
import { gsap } from "gsap";
import { Flip } from "gsap/Flip";
gsap.registerPlugin(Flip);
const contenedor = document.querySelector(".contenedor");
const tarjetas = gsap.utils.toArray(".tarjeta");
document.querySelector("#cambiar").addEventListener("click", () => {
const estado = Flip.getState(tarjetas);
contenedor.classList.toggle("cuadricula");
contenedor.classList.toggle("lista");
Flip.from(estado, {
duration: 0.6,
ease: "power2.inOut",
stagger: 0.03,
absolute: true,
});
});
Ese código funciona pegado tal cual. El absolute: true es lo que evita que las tarjetas se empujen entre sí durante el movimiento: sin él, en cuanto la primera se coloca en su posición nueva, las demás se recolocan bruscamente en el flujo mientras sus transformaciones intentan animarlas.
Fíjate en que no se anima nada del contenedor. El contenedor cambia de clase instantáneamente; lo que se anima son los hijos, y solo su geometría.
Caso dos: de miniatura a detalle
El segundo patrón más habitual: una tarjeta pequeña de una lista que se expande hasta ocupar un panel de detalle. Es el mismo mecanismo con un cambio de clase distinto.
const tarjeta = document.querySelector(".tarjeta-3");
tarjeta.addEventListener("click", () => {
const estado = Flip.getState(tarjeta, { props: "borderRadius,backgroundColor" });
tarjeta.classList.toggle("detalle");
Flip.from(estado, {
duration: 0.55,
ease: "power3.inOut",
absolute: true,
zIndex: 100,
onComplete: () => tarjeta.classList.toggle("listo"),
});
});
.tarjeta { border-radius: 8px; }
.tarjeta.detalle {
position: fixed;
inset: 5vh 5vw;
border-radius: 24px;
z-index: 50;
}
zIndex: 100 durante la animación evita que la tarjeta pase por debajo de sus hermanas mientras crece. props incluye el radio del borde para que la esquina se redondee progresivamente en lugar de saltar. Y el cambio a position: fixed es exactamente el tipo de cosa que ninguna transición de CSS podría animar.
Caso tres: objetivos anidados
Si en la captura entran a la vez un contenedor y sus hijos, aparece un problema aritmético: cuando el padre se desplaza, los hijos se desplazan con él, así que si Flip también les aplica su propio desplazamiento, se mueven el doble.
nested: true le pide al plugin que haga los cálculos adicionales para descontar el movimiento heredado.
const estado = Flip.getState(".panel, .panel .tarjeta");
reorganizar();
Flip.from(estado, { nested: true, duration: 0.6, absolute: true });
Es una opción que cuesta cálculo, así que solo se activa cuando de verdad hay anidamiento en los objetivos. Y hay una alternativa que suele ser mejor: capturar solo el nivel que de verdad necesitas animar. Si los hijos van a acompañar a su padre sin movimiento propio, no los metas en la captura.
La documentación es explícita: rotationX, rotationY y z no están soportados. Flip trabaja con posición, tamaño, rotación en el plano y sesgado. Si tu transición implica giros en profundidad, tendrás que componerla a mano por encima de un FLIP en dos dimensiones, o replantearla.
Lo que no arregla Flip
Merece la pena delimitarlo. Flip anima la geometría de los elementos que le indicas, y nada más.
Si el cambio de layout hace que el texto se recomponga —una tarjeta que pasa de tres líneas a una— Flip no puede animar esa recomposición: el texto salta a su composición nueva desde el primer frame y solo la caja se mueve suavemente. Es visible cuando el cambio de anchura es grande.
Si el cambio hace que aparezcan o desaparezcan hijos, esos hijos no tienen medida de origen o de destino, y quedan fuera de la animación salvo que uses los callbacks correspondientes.
Y si el contenedor tiene overflow con scroll, la posición de scroll no forma parte del estado capturado: si el contenedor estaba desplazado y deja de estarlo, la geometría medida y la percibida no coinciden.
Hay una petición recurrente en los foros de especificación que consiste en pedir que display sea animable, y merece la pena entender por qué nunca va a concederse en la forma en que la gente la pide. Interpolar entre dos valores requiere que exista una noción de punto intermedio, y esa noción existe para los números, para los colores y para las longitudes porque todos viven en espacios donde la media aritmética significa algo. grid y flex no son valores de un espacio: son nombres de dos procedimientos distintos, con conjuntos de propiedades distintos, invariantes distintos y estructuras de datos distintas. Un “cincuenta por ciento de grid y cincuenta por ciento de flex” no está mal definido por descuido de la especificación; está mal definido de la misma manera que lo está la media entre una ordenación por burbuja y una por mezcla. La única interpolación que sí tiene sentido, y que es la que la gente realmente quiere cuando lo pide, es la del resultado: dado el rectángulo que ocupa cada elemento bajo un algoritmo y el que ocupa bajo el otro, interpolar entre esos dos rectángulos. Y eso es exactamente lo que hace FLIP, así que el patrón no es un apaño para suplir una carencia de CSS, es la formulación correcta del problema que estaba mal planteado. La misma idea vuelve a aparecer, con otra fachada, en las View Transitions del navegador: capturar el resultado antes, capturar el resultado después, interpolar entre las capturas y no entre las causas. Cuando reconozcas ese patrón —interpolar salidas en lugar de entradas cuando las entradas no viven en un espacio continuo— lo vas a ver en sitios que no tienen nada que ver con animación: en el diffing de árboles, en la reconciliación de estados y en cualquier sistema que tenga que mostrar una transición entre dos configuraciones discretas.
- Monta el alternador de cuadrícula y lista del ejemplo y quita
absolute: true. Describe exactamente qué artefacto aparece. - Añade una tarjeta que se expanda a detalle con
propsincluyendo el radio y observa la diferencia respecto a no incluirlo. - Captura a la vez un contenedor y sus hijos sin
nested: truey mide cuánto se pasan de largo. - Provoca el caso del texto que se recompone: una tarjeta que pasa de estrecha a ancha con tres líneas de texto. Decide si el artefacto es aceptable.
- Combina el alternador de vista con
prune: truey comprueba si alguna tarjeta queda fuera de la animación.