Scaling Element on Scroll (GSAP Flip)

Uses GSAP Flip and ScrollTrigger to seamlessly scale and reposition a single target element between multiple wrapper waypoints as the user scrolls. Flip calculates the size and position difference between wrappers and animates the transition scrubbed to scroll progress.

gsapflipscrolltriggerscrollscalelayout

Setup — External Scripts

Setup: External Scripts
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/Flip.min.js"></script>

Code

index.html
html
<div class="resource-wrapper">
  <section class="scaling-element-header"><span class="scaling-element-header__eyebrow">[ Resource ]</span>
    <h1 class="scaling-element-header__h1">Scaling element on scroll with Flip</h1>
    <div class="scaling-element__small-box">
      <div class="scaling-video__before"></div>
      <div data-flip-element="wrapper" class="scaling-video__wrapper">
        <div data-flip-element="target" class="scaling-video">
          <video autoplay="autoplay" muted="" playsinline="" loop="" webkit-playsinline="" class="scaling-video__video"><source src="https://osmo.b-cdn.net/resource-media/scaling-element-resource-185787-876545918_tiny.mp4" type="video/mp4"></video>
          <svg xmlns="http://www.w3.org/2000/svg" width="100%" viewbox="0 0 138 138" fill="none" class="scaling-video__svg"><path d="M81.7432 46.534C79.5777 48.6995 75.875 47.1659 75.875 44.1034V0.25H62.125V51.8124C62.125 57.5079 57.5079 62.1249 51.8125 62.1249H0.25V75.8749H44.1034C47.1659 75.8749 48.6996 79.5776 46.5341 81.7431L16.0136 112.263L25.7364 121.986L56.2569 91.466C58.416 89.3069 62.1031 90.825 62.125 93.8693V137.75H75.8751L75.875 86.1874C75.875 80.492 80.4921 75.8749 86.1875 75.8749H137.75V62.1249H93.8692C90.8339 62.1031 89.3157 58.4375 91.4469 56.2759L91.4659 56.2569L121.986 25.7363L112.264 16.0137L81.7432 46.534Z" fill="currentColor"></path></svg>
        </div>
      </div>
    </div>
  </section>
  <section class="scaling-element-video">
    <div class="scaling-element__big-box">
      <div class="scaling-video__before"></div>
      <div data-flip-element="wrapper" class="scaling-video__wrapper"></div>
    </div>
    <h1 class="scaling-element-header__h1">And you can have more content here ...</h1>
  </section>
</div>
styles.css
css
.resource-wrapper {
  position: relative;
  overflow: hidden;
}

.scaling-element-header {
  grid-column-gap: 3em;
  grid-row-gap: 3em;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  padding: 25vh 5vw 20vh;
  display: flex;
  position: relative;
}

.scaling-element-header__h1 {
  text-align: center;
  max-width: 9em;
  margin-top: 0;
  margin-bottom: .25em;
  font-size: 7em;
  font-weight: 500;
  line-height: 1;
}

.scaling-element-header__eyebrow {
  color: #9d420a;
  text-transform: uppercase;
  font-size: 1.25em;
  font-weight: 400;
}

.scaling-element-video {
  grid-column-gap: 25vh;
  grid-row-gap: 25vh;
  flex-flow: column;
  justify-content: center;
  align-items: center;
  padding-bottom: 25vh;
  padding-left: 5vw;
  padding-right: 5vw;
  display: flex;
  position: relative;
}

.scaling-element__big-box {
  border-radius: 1em;
  width: 100%;
  position: relative;
}

.scaling-element__small-box {
  border-radius: 1em;
  width: 20em;
  position: relative;
}

.scaling-video__wrapper {
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
}

.scaling-video {
  will-change: transform;
  background-color: #d2800f;
  border-radius: 1em;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
  top: 0;
  left: 0;
  overflow: hidden;
  isolation: isolate;
  transform: translateX(0) rotate(0.001deg);
}

.scaling-video__before {
  padding-top: 56.25%;
}

.scaling-video__video {
  object-fit: cover;
  width: 100%;
  height: 100%;
  padding-bottom: 0;
  padding-right: 0;
  position: absolute;
  border-radius: inherit;
}

.scaling-video__svg {
  color: #fff;
  mix-blend-mode: overlay;
  width: 6.25em;
  position: absolute;
}

@media screen and (max-width: 767px) {
  .scaling-element-header__h1 {
    font-size: 13.5vw;
  }

  .scaling-element__small-box {
    width: 15em;
  }

  .scaling-video__svg {
    width: 5em;
  }
}
script.js
javascript
gsap.registerPlugin(ScrollTrigger, Flip);

function initFlipOnScroll() {
  let wrapperElements = document.querySelectorAll("[data-flip-element='wrapper']");
  let targetEl = document.querySelector("[data-flip-element='target']");

  let tl;
  function flipTimeline() {
    if (tl) {
      tl.kill();
      gsap.set(targetEl, { clearProps: "all" });
    }

    // Use the first and last wrapper elements for the scroll trigger.
    tl = gsap.timeline({
      scrollTrigger: {
        trigger: wrapperElements[0],
        start: "center center",
        endTrigger: wrapperElements[wrapperElements.length - 1],
        end: "center center",
        scrub: 0.25
      }
    });

    // Loop through each wrapper element.
    wrapperElements.forEach(function(element, index) {
      let nextIndex = index + 1;
      if (nextIndex < wrapperElements.length) {
        let nextWrapperEl = wrapperElements[nextIndex];
        // Calculate vertical center positions relative to the document.
        let nextRect = nextWrapperEl.getBoundingClientRect();
        let thisRect = element.getBoundingClientRect();
        let nextDistance = nextRect.top + window.pageYOffset + nextWrapperEl.offsetHeight / 2;
        let thisDistance = thisRect.top + window.pageYOffset + element.offsetHeight / 2;
        let offset = nextDistance - thisDistance;
        // Add the Flip.fit tween to the timeline.
        tl.add(
          Flip.fit(targetEl, nextWrapperEl, {
            duration: offset,
            ease: "none"
          })
        );
      }
    });
  }

  flipTimeline();

  let resizeTimer;
  window.addEventListener("resize", function () {
    clearTimeout(resizeTimer);
    resizeTimer = setTimeout(function () {
      flipTimeline();
    }, 100);
  });
}

// Initialize Scaling Elements on Scroll (GSAP Flip)
document.addEventListener('DOMContentLoaded', function() {
  initFlipOnScroll();
});

Guide

Overview

GSAP Flip records the size and position of the target element at each wrapper waypoint, then builds a scrubbed timeline that transitions between them as the user scrolls. The result is a smooth, layout-aware scale and reposition with no manual coordinate math.

Wrapper Attribute

Add data-flip-element="wrapper" to every section that acts as a waypoint. You need at least two. The script reads each wrapper's size and position to compute the transition distance. Add more wrappers to create additional waypoints the target snaps through in sequence.

Target Attribute

Place the element you want to animate as a direct child of the first wrapper and add data-flip-element="target". Only one target per page is supported. All other wrappers stay empty — they serve as size/position references only.

Scrub

scrub: 0.25 gives a small lag that feels connected to scroll without being loose. Set to true for instant lock or a higher number (e.g. 1) for more drag. Pairs well with Lenis smooth scroll.

Extra Flip Options

absolute: true solves layout issues with flexbox or grid containers. scale: true resizes via scaleX/scaleY instead of width/height. ease can be changed per tween (e.g. "power1.inOut"). See the GSAP Flip documentation for the full option list.

Resize Handling

On resize the timeline is killed, inline styles are cleared, and flipTimeline() rebuilds everything from current DOM measurements. A 100ms debounce prevents excessive recalculation during drag.