Master While-Scrolling Animations in Webflow (No Code)

Advanced 47:49 webflowwhile-scrollinginteractionsanimationcmsno-code

Recreate an award-winning while-scrolling scaling gallery in Webflow using native interactions, absolute-positioned triggers, and a calc() trick — no code at all.

Key takeaways

  • The direction-dependent transform origin isn't one hard problem — it's two easy ones. Put two images in each cell, each with its own transform origin, and show the right one at the right time.
  • You can't reliably compute an image's height from the viewport height across every device — so stop trying. Flip the problem to the empty space above or below it, whose height is always calc(100vh - 100%).
  • An absolutely-positioned trigger inside the relative image wrapper gives the 0% and 100% of a while-scrolling timeline a concrete, device-independent meaning: fully in view, and just starting to leave.
  • Fill the gaps in an irregular grid with empty div blocks instead of hand-placing every image with grid-column — it turns a placement nightmare into a layout you can read at a glance.
  • Custom elements plus custom attributes are how you break the one-class-per-collection rule: they let each CMS item carry its own classes and its own grid position through calc() on CMS number fields.

Video chapters

  1. 00:00 Intro
  2. 01:18 Notes on the approach to the layout and animation
  3. 03:09 Building the layout, part 1
  4. 15:10 The right approach to the while-scrolling animation, part 1
  5. 19:22 Building the layout, part 2
  6. 22:46 Building the animation, part 1
  7. 27:04 The right approach to the while-scrolling animation, part 2
  8. 28:55 Building the animation, part 2
  9. 32:38 Layout: final touches
  10. 33:58 Animation preview
  11. 34:32 Applying the layout to a CMS collection and to different aspect ratios
  12. 44:05 Responsiveness
  13. 46:27 Animation preview after responsiveness
  14. 47:15 Outro

You can rebuild an award-winning while-scrolling animation in Webflow with no code at all — a gallery where each image scales up as it enters the viewport and scales back down as it leaves. The catch isn’t the animation itself. It’s the layout. Get the structure right and the interactions become almost trivial; get it wrong and no amount of tweaking will make it feel smooth on every screen.

Looking closely at the original, three things make it sneaky. The transform origin changes depending on whether an image is scaling in from the bottom or out at the top. Every image starts scaling the moment it scrolls into view and stops when it’s fully visible, regardless of screen size or aspect ratio. And the scaling makes each image feel like it briefly sticks to the edge of the viewport. None of these has an obvious native solution — until you change perspective.

That change of perspective is the whole lesson. A little lateral thinking, absolute positioning, one calc() expression, and Webflow’s native while-scrolling interactions are enough to push the platform surprisingly far.

How it works

Two images beat one impossible one. The transform origin has to differ for scaling in versus scaling out, and a single element can’t hold two origins. So each grid cell gets two copies of the image, wrapped in a div that becomes the real cell. One copy (cc1) carries the origin for scaling up from the bottom; the other (cc2) carries the origin for scaling down from the top. Only one is visible at any moment — you toggle them with a simple scroll-into-view interaction. The hard problem dissolves into two easy ones.

The gray rectangle — the key mental model. A while-scrolling timeline measures its 0%–100% against the viewport height, but an image’s height is driven by the viewport width, so there’s no percentage that stays correct across devices. The fix is to stop animating against the image and animate against the empty space instead. Picture the image sitting at the bottom of the viewport and a gray rectangle filling the space above it: that rectangle is fully visible exactly when the image should start scaling, and starts leaving exactly when the image is fully grown. Its height, by definition, is the viewport minus the image — calc(100vh - 100%) inside a wrapper set to the image’s height. That’s device-independent, and it gives 0% and 100% a concrete meaning at last.

Triggers, not images, drive the timeline. You build that rectangle as an absolutely-positioned div (the “trigger”) inside a position: relative image wrapper. One trigger sits above the image for the scale-up, a mirrored one sits below for the scale-down. The while-scrolling interaction is applied to the trigger with offsets set to “start when fully in view, end when it starts leaving.” The animation targets the trigger’s sibling image, so a single interaction, applied by class, drives every cell in the grid at once.

Empty cells as div blocks. The gallery grid is irregular — most cells are empty. Rather than hand-placing each image with grid positioning (a nightmare at scale), you fill occupied cells with images and empty cells with empty div blocks, in reading order. Filling the grid becomes a breeze.

CMS needs custom elements + custom attributes. A collection list can’t give each item its own combo class or grid slot from the Designer. The workaround pairs two of Webflow’s most powerful features: wrap each image in a custom element and bind its class attribute to a CMS text field (so each item can carry different transform-origin classes), and give each item a custom tag attribute equal to its slug so a code embed can target items one by one and set their margins from CMS number fields.

How to use it

  1. Placeholder section. Add a section (no styling) with a container and a content wrapper set to display: flex, centered, height: 100vh. It simply hides the gallery below the fold.

  2. Gallery grid. Add a second section with a container and a content wrapper set to display: grid, four columns, gap: 25vw. Fill occupied cells with an img (width 100%) and every empty cell with an empty div block, matching the original’s layout row by row.

  3. Double every image. Wrap each image in a div (this becomes the cell), then duplicate the image inside it. Give the first copy the cc1 combo class and the second cc2 (set cc2 to display: none). Then add transform-origin combo classes per copy — e.g. cc-top-left / cc-top-right for the scale-up copies and cc-bottom-left / cc-bottom-right for the scale-down copies — reading the correct corners off the original for each cell.

  4. Build the triggers. Set the image wrapper to position: relative. Inside it, add a div (position: absolute, width: 100%) and give it a height of:

    height: calc(100vh - 100%);

    Give it a cc-above combo class and a bottom offset of 100% so it sits above the image. Duplicate it as cc-below with a top offset of 100% so it sits below.

  5. Scale-up animation. Select the above trigger and add a while-scrolling interaction (play scroll interaction), set to start when the element is fully in view and end when it starts scrolling out. Add one scale action on the sibling image: 0 at the 0% keyframe, 1 at 100%. Target the plain gallery-section-image class (no combos) so both images in the cell scale, and set the trigger to apply by class — then copy the trigger into every cell.

  6. Scale-down animation. On the below trigger, add the same while-scrolling interaction but invert the scale: 1 at 0%, 0 at 100%. Apply by class and copy into every cell.

  7. Swap the visible image. Add a scroll-into-view interaction on the above trigger with a hide/show action group: show cc1 (set to block), hide cc2 (set to none). Add an empty scroll-out-of-view animation so the interaction resets and replays each pass. Do the mirror on the below trigger (show cc2, hide cc1).

  8. Clean up. Reset the triggers’ test opacity and background, and set pointer-events: none on the trigger’s base class so it never blocks clicks.

Making it CMS-driven

  1. Flex, not grid. For a collection, set the list to the same content-wrapper class plus a cc-flex combo: display: flex, wrap, aligned top-left. Give each collection item the image-wrapper class plus cc-flex and this width:

    /* 4 items per row, 3 gaps of 25vw between them */
    width: calc((100% - 75vw) / 4);
  2. Bind classes via custom elements. Wrap each of the two images in a custom element and bind its class attribute to a CMS text field holding the exact base + combo class names (space-separated). Apply the scale transform to these wrappers rather than the images directly.

  3. Position each item from the CMS. Add margin-right and margin-left number fields to the collection and give each item a custom tag attribute equal to its slug. In a code embed, target that attribute and set the horizontal margins from the fields, where each empty cell is worth 25% of a row:

    /* margin-left / margin-right are CMS number fields (count of empty cells) */
    margin: 0 calc(var(--margin-right) * 25%) 0 calc(var(--margin-left) * 25%);
  4. Responsiveness. On portrait, drop the grid to two columns and set empty cells to display: none; for the CMS version, override the margins to 0, set the item width to calc((100% - 25vw) / 2), stretch the wrappers on the Y axis, and add an img-cover class (object-fit: cover) so mixed aspect ratios still fill neatly.

Resources

Frequently asked questions

How do you change an image's transform origin depending on scroll direction in Webflow?
You can't animate a single element with two different transform origins, so you don't try. Put two copies of the image in the cell — one with its origin set for scaling up from the bottom of the viewport, one for scaling down from the top — then use a scroll-into-view interaction to show the correct copy and hide the other as each trigger enters view.
Why use an absolute-positioned trigger element instead of animating the image directly?
A while-scrolling timeline's 0% and 100% are measured against the viewport height, but an image's height depends on the viewport width, so no fixed percentage works across devices. A trigger whose height is calc(100vh - 100%) — the viewport minus the image — is fully in view exactly when the image should start scaling and starts leaving exactly when it should finish, giving those percentages a stable meaning on any screen.
How do you build an irregular image grid in Webflow without manually positioning every cell?
Set the wrapper to a grid, then fill every occupied cell with an image and every gap with an empty div block. Placing empty divs in reading order is far easier to build and maintain than assigning grid-column and grid-row to each image by hand, especially once you have dozens of images.
How can each item in a Webflow CMS collection get a different class and grid position?
You can't assign per-item combo classes from the Designer — a class change hits every item in the list. Instead wrap each image in a custom element and bind its class attribute to a CMS text field, and give each item a custom tag attribute set to its slug so a code embed can target it individually and set its margins from CMS number fields via calc().

← Back to the course