wandres.dev
GSAP IX · Flip

Flip.getState y Flip.from: el ciclo completo

Qué captura exactamente el estado, cómo se le añaden propiedades, y el catálogo de opciones de Flip.from con las cuatro que de verdad usarás siempre.

⏱ 19 min

Todo el plugin cabe en dos llamadas y una línea de código propio entre ellas. Esa simplicidad aparente esconde decisiones que hay que tomar: qué elementos entran en la captura, qué propiedades se recuerdan además de la geometría, si el cambio de tamaño se hará escalando o redimensionando, y si los elementos deben salir del flujo mientras dura el movimiento. Cada una de esas decisiones tiene un valor por defecto razonable y un caso en el que el valor por defecto es exactamente lo que no quieres.

🎯 Al terminar esta lección sabrás
  • Capturar un estado con Flip.getState() y saber qué guarda por defecto.
  • Añadir propiedades no geométricas a la captura con props.
  • Elegir entre scale y el redimensionado por defecto según el contenido.
  • Reconocer cuándo hace falta absolute y qué efecto secundario tiene.

Los tres pasos

import { gsap } from "gsap";
import { Flip } from "gsap/Flip";

gsap.registerPlugin(Flip);

// 1) Capturar
const estado = Flip.getState(".tarjeta");

// 2) Cambiar lo que sea: clases, orden, contenedor, estilos
document.querySelector(".panel").classList.toggle("expandido");

// 3) Animar desde el estado capturado hasta el actual
Flip.from(estado, { duration: 0.5, ease: "power2.inOut" });

Flip.getState() acepta texto de selector, un elemento, un array o un NodeList. Por defecto captura posición en el viewport, tamaño, rotación, sesgado y opacidad de cada objetivo. No captura color, ni fondo, ni radio de borde, ni nada más.

Hay un matiz importante: si en el momento de capturar hay un FLIP activo sobre alguno de esos elementos, getState() lo fuerza al final antes de medir, para no capturar una posición intermedia. Es lo que permite encadenar transiciones rápidas sin que se acumulen errores.

El paso intermedio no tiene que pasar por el plugin. Puedes hacer lo que quieras: mover nodos, alternar clases, cambiar estilos en línea, reordenar. La única condición es que el cambio esté aplicado cuando llames a Flip.from(), lo cual en frameworks que renderizan de forma diferida requiere esperar al siguiente ciclo.

Flip.from() devuelve una timeline, así que puedes controlarla, pausarla, invertirla o añadirle otras animaciones con add().

const tl = Flip.from(estado, { duration: 0.5 });
tl.from(".detalle", { autoAlpha: 0, y: 20 }, "-=0.2");

props: más allá de la geometría

Por defecto, un FLIP no anima el color de fondo aunque cambie: el elemento salta a su color nuevo mientras se desplaza suavemente. Para incluir propiedades adicionales hay que declararlas en la captura, porque si no se guardó el valor anterior no hay nada desde donde animar.

const estado = Flip.getState(".tarjeta", {
  props: "backgroundColor,color,borderRadius",
});

tarjeta.classList.toggle("destacada");

Flip.from(estado, { duration: 0.5 });

La lista va separada por comas y en formato camelCase, no con guiones. Flip.from() acepta también una opción props, pero su función es distinta: sirve para limitar la animación a un subconjunto de las capturadas, no para añadir. Si no la pones, se animan todas las que estén en el estado.

getState() acepta además simple: true, que le dice al plugin que no hay contenedores rotados, escalados ni sesgados en el camino y que puede saltarse los cálculos de compensación. Es una promesa que haces tú; si mientes, las posiciones salen mal. En la mayoría de las páginas la diferencia de rendimiento es imperceptible, y solo compensa activarlo con muchos elementos.

scale: la decisión que cambia el resultado

Por defecto, Flip anima width y height para cambiar el tamaño. Con scale: true, usa scaleX y scaleY.

Las dos opciones tienen ventajas opuestas y elegir mal se nota mucho.

Redimensionar —el valor por defecto— reflowea el contenido en cada frame: el texto se recompone, las imágenes se recortan, los hijos se reposicionan. El resultado es correcto en todo momento y cuesta layout en cada frame.

Escalar deforma: el contenido se estira o se comprime junto con la caja. Es mucho más barato porque lo resuelve el compositor, pero un texto escalado en horizontal a 1,8 durante medio segundo se ve mal.

// Tarjeta con texto: redimensionar, aunque cueste mas
Flip.from(estado, { duration: 0.6 });

// Miniatura de imagen que crece a pantalla completa: escalar
Flip.from(estado, { duration: 0.6, scale: true });

La regla práctica: escala cuando el contenido sea una imagen o un color plano, y redimensiona cuando haya texto. Si tienes texto y necesitas escalar por rendimiento, el truco habitual es escalar el contenedor y aplicar la escala inversa al texto para compensar, aunque eso ya es un montaje a medida.

⚠️
box-sizing importa

La documentación recomienda explícitamente box-sizing: border-box en los elementos que vayan a participar en un FLIP. Con el modelo de caja por defecto, el width que Flip mide y anima no incluye relleno ni borde, y cualquier diferencia de relleno entre el estado inicial y el final produce un salto al final de la animación.

absolute: cuando el layout se colapsa

Este es el ajuste que más veces salva una transición y el que más gente descubre por accidente.

El problema aparece con contenedores flex y grid. Mientras un elemento se desplaza de una posición a otra, sus hermanos siguen ocupando el espacio del layout nuevo, así que el contenedor ya está reorganizado desde el frame uno. Eso suele estar bien, pero produce artefactos cuando el elemento que se mueve cambiaba el tamaño del contenedor, o cuando varios elementos se cruzan.

absolute: true pone todos los objetivos en position: absolute durante la animación, sacándolos del flujo. Así se mueven libremente sin afectar ni ser afectados por el layout de los demás.

El efecto secundario es inmediato: al salir del flujo, dejan de ocupar espacio, y si eran los que daban altura a su contenedor, ese contenedor se colapsa. Para eso absolute acepta también un selector o un array, de modo que puedes sacar del flujo solo un subconjunto y dejar dentro al que sostiene el layout.

// Todos fuera del flujo
Flip.from(estado, { absolute: true, duration: 0.5 });

// Solo las tarjetas; el contenedor se queda en el flujo sosteniendo la altura
Flip.from(estado, { absolute: ".tarjeta", duration: 0.5 });

Un aviso de la documentación que conviene tener presente: con absolute: true, las coordenadas se calculan respecto al viewport actual, así que si el usuario hace scroll o redimensiona durante la animación, la posición puede desviarse. Al acabar, todo queda donde debe.

El resto de opciones, por orden de utilidad

Flip.from() acepta además cualquier propiedad de tween normal: duration, ease, stagger, onComplete, repeat. Y estas propias:

Opción Para qué
nested: true Si hay padres e hijos en los objetivos, evita que los desplazamientos se acumulen
prune: true Descarta los objetivos que no se han movido, para ahorrar y para que el stagger no cuente huecos
toggleClass Añade una clase mientras dura la animación, y la retira al acabar
zIndex Fija un zIndex durante la animación para que los objetivos queden por encima
spin Añade vueltas completas. Acepta booleano, número o función por objetivo
targets Anima un subconjunto del estado, o unos elementos nuevos que se correlacionan por identificador

prune merece un comentario porque su efecto sobre el stagger es el que lo hace útil de verdad. Al filtrar una lista de veinte tarjetas y quedarte con seis, las catorce que no se mueven seguirían contando para el escalonado, produciendo huecos temporales sin nada visible. Con prune: true desaparecen del cálculo y el escalonado se aplica solo a las que de verdad se mueven.

Flip.from(estado, {
  duration: 0.5,
  ease: "power2.inOut",
  absolute: ".tarjeta",
  prune: true,
  stagger: 0.04,
  toggleClass: "en-movimiento",
});
Flip.getState no captura elementos: captura una fotografía de un instante, y esa distinción decide si tu integración funciona

La confusión más costosa con este plugin no es sintáctica, es conceptual, y sale a la luz en cuanto lo metes en un framework. El objeto que devuelve getState() parece una referencia a unos elementos, y en realidad es un registro con dos partes: los datos geométricos de un instante, y las identidades de los elementos a los que corresponden esos datos. Mientras la fase Last se limite a mover o restilar los mismos nodos, ambas partes siguen siendo válidas y todo funciona sin pensar. Pero en cuanto la fase Last consiste en un rerenderizado —React, Vue, Solid— los nodos viejos se han eliminado del DOM y hay nodos nuevos con el mismo aspecto y distinta identidad. El estado sigue teniendo la geometría correcta y ya no tiene a quién aplicársela, así que Flip no anima nada, y no lanza ningún error porque desde su punto de vista no ha pasado nada anómalo. La solución oficial es exactamente la que se deduce del párrafo anterior y no es un parche: pasar targets para señalar a los nodos nuevos, y data-flip-id para que Flip pueda casar cada nodo nuevo con la geometría del viejo. Es decir, reponer a mano la parte de identidad que el rerenderizado destruyó. La lección va más allá de Flip. Cada vez que una biblioteca te da un objeto que “captura” algo del DOM, pregúntate qué parte de esa captura son datos y qué parte son referencias, y qué operaciones de tu aplicación invalidan cada parte. Los datos sobreviven a casi todo; las referencias no sobreviven a un rerenderizado, a un innerHTML, a una navegación ni a un replaceChildren. Casi todos los bugs de integración de bibliotecas de animación con frameworks reactivos son exactamente esta confusión, con distintos nombres.

⚔️ El ciclo completo, opción a opción
  1. Monta un FLIP básico que alterne una clase de expansión y comprueba que la timeline devuelta se puede invertir con reverse().
  2. Añade un cambio de color de fondo y observa que salta. Captúralo con props y compáralo.
  3. Prueba la misma transición con scale: true y sin él sobre una tarjeta con texto. Decide cuál usarías.
  4. Provoca el colapso del contenedor con absolute: true y arréglalo restringiendo el selector.
  5. Filtra una lista de veinte elementos a seis con stagger y sin prune, y luego con prune: true. Describe la diferencia en el escalonado.