D
P
0

CSS & Web Animation

GSAP Flip `absolute:true` Collapsed My Grid and Tiles Overlapped the Next Section?

July 30, 2026·5 min read
GSAP Flip `absolute:true` Collapsed My Grid and Tiles Overlapped the Next Section?

A client site I was building had a signature moment on the homepage: a row of sector tiles packed tightly as a strip, then on scroll they morph into larger feature cards. I built the animation with GSAP Flip, because Flip is great at measuring an element's start and end positions and tweening between them without me computing any coordinates by hand. Duration 2 seconds, ease expo.inOut, stagger 0.05. In my head it was smooth. On screen it was a mess.

The instant the morph kicked off, the grid height collapsed to almost nothing, and the animating tiles piled down on top of the About section right below it. So it was not just ugly: it broke the whole page flow. Everything after the grid got sucked upward for a full two seconds, then snapped back to normal exactly when the animation finished. It reset precisely at the animation duration. That was the first clue I should have read sooner.

Tracing it

My first reflex aimed at the wrong target: I assumed this was purely a CSS Grid problem. I poked at min-height on the container, I set grid-auto-rows, I tried to pin the grid height with aspect-ratio. None of it mattered. The grid height still crumpled the moment the morph started, and once it finished everything came back like nothing ever happened.

Because the problem lived and died exactly on the animation timeline, I stopped blaming static CSS and started inspecting the DOM while the morph was actually running. I dropped a small log to read the grid height mid-animation:

Flip.from(state, {
  duration: 2,
  onUpdate: () => {
    console.log(grid.offsetHeight, getComputedStyle(grid).position);
  },
});

The result was blunt: for those two seconds, grid.offsetHeight dropped to a tiny number, and the morphing tiles carried a position: absolute I never wrote in CSS. I had not put position: absolute anywhere near them. So something was injecting it at runtime. And only one thing here touches element positioning: Flip itself.

The root cause

The culprit was one small option in the Flip config that I had copied straight from a reference design site:

// The version that collapsed the grid
const state = Flip.getState(".morph-tile");
grid.classList.add("is-feature"); // strip -> feature layout
 
Flip.from(state, {
  duration: 2,
  ease: "expo.inOut",
  stagger: 0.05,
  absolute: true, // <- this is the culprit
});

absolute: true tells Flip to position elements absolutely during the animation. The idea actually makes sense: with absolute, a moving tile no longer shoves its siblings around, so the tween can stay smooth without the layout jittering every frame. But there is a consequence I did not account for. Absolute elements leave the document flow. For those two seconds, my grid lost every child that contributed height, because all of them were floating outside the flow. A grid with no in-flow children collapses to zero height. And since the tiles were now absolute relative to the context above them, they spilled downward and covered the next section. Two symptoms that looked separate turned out to be one root: the collapsed height and the overlapping tiles are two faces of the same thing.

So why does the reference site survive absolute: true? I dug through its markup, and the answer was this: it re-parents the flipped elements into a dedicated container whose height is reserved ahead of time. Because that container holds its own height, tiles leaving the grid flow collapse nothing. I copied the option but not the container architecture behind it. That is why it worked there and broke for me.

The fix

The fix is not to patch the grid height with a magic number, it is to run Flip in flow. Drop absolute: true. As long as the tiles stay in flow, the grid never loses its height, so there is nothing to force with min-height:

// Kill any in-progress Flip so getState reads the final
// positions, not a mid-tween state from a previous run
Flip.killFlipsOf(".morph-tile");
 
const state = Flip.getState(".morph-tile");
grid.classList.add("is-feature");
 
Flip.from(state, {
  duration: 2,        // identical to the reference
  ease: "expo.inOut", // identical
  stagger: 0.05,      // identical
  // no absolute:true -> tiles stay in flow, grid height is safe
  onComplete: () => ScrollTrigger.refresh(),
});

Those two extra lines matter. Flip.killFlipsOf(".morph-tile") before getState makes sure that if a user scrolls back and forth quickly, Flip does not read a state from tiles still mid-tween in an older animation, which would give it a wrong starting position and make the tween jump. And ScrollTrigger.refresh() in onComplete tells ScrollTrigger to recompute every trigger position, because the page height changed after the strip layout became the feature layout. Without the refresh, triggers below the grid shift and the next animation fires at the wrong point.

What I kept verbatim: duration, ease, and stagger stayed exactly as the reference had them. The only thing I removed was one option that did not fit my DOM structure. After that the morph was smooth, the grid never collapsed, and the boundary between the grid and the section below it was clean from the first frame to the last.

The takeaway

  • If a layout collapses for exactly the animation duration and then snaps back, suspect the animation, not your static CSS.
  • absolute: true in GSAP Flip pulls elements out of flow. A container whose children are being flipped can lose its height and collapse to zero.
  • Read the DOM while the animation runs via onUpdate, not just in a resting state. A position: absolute you never wrote is a big clue.
  • Copying animation options from another site without copying its container architecture is a trap. The same option needs the same DOM structure to behave the same way.
  • Run Flip in flow when you can. Call Flip.killFlipsOf() before getState, and ScrollTrigger.refresh() in onComplete so triggers below adjust to the new height.