Dynamic Image-Stacking Hover Animation in Webflow

Advanced 50:21 webflowcmshoveranimationinteractionsjavascript

Recreate an award-winning, CMS-driven hover animation in Webflow where preview images reveal, scale, and stack on top of each other with minimal custom code.

Key takeaways

  • The three-wrapper trick: stack a white, a black, and a second white text wrapper, then clip the item with overflow:hidden — so hovering in or out from either direction always reveals the right one.
  • Webflow can't tell which way you hovered from. Split the item into an absolutely-positioned top-half and bottom-half trigger, and let each fire its own animation.
  • Fixing the preview image to the viewport (position: fixed + pointer-events: none) lifts it out of the clipped item and lets one floating image sit in a corner across every list row.
  • Webflow's click interaction plays a different animation on the first click than the second — abuse that as a two-state counter to animate two images per item with no counting logic of your own.
  • A dozen lines of JavaScript do only what Webflow cannot: on mouse-enter they "click" a hidden trigger and bump the hovered image's z-index, so the newest preview always stacks on top.

Video chapters

  1. 00:00 Intro
  2. 01:01 Building the layout, part 1
  3. 03:54 Building the CMS collection
  4. 06:59 Building the layout, part 2
  5. 17:37 The hover animation (no-code): the wrong way
  6. 21:00 The hover animation (no-code): the right way
  7. 28:40 Building the layout, part 3
  8. 29:38 The hover animation: image scaling
  9. 32:27 Enhancing the hover animation with JavaScript
  10. 49:37 Outro

You can build a dynamic, image-stacking hover animation in Webflow — the kind you see on award-winning agency sites — almost entirely with native interactions, plus a dozen lines of JavaScript for the one thing Webflow can’t do alone. Hover a row in a list and a preview image scales up in the corner; move down the list and each new image stacks on top of the last, while the row’s text slides to reveal a black-on-white variant underneath.

It looks fancy, and it is — but it’s really a stack of small, honest decisions. A CMS collection so the whole thing is content-driven. A clever three-wrapper layout so the text reveal reads correctly no matter which direction your cursor comes from. And a tiny script that borrows Webflow’s own click interaction to fake something it has no native equivalent for.

The build is worth following start to finish, because the interesting part isn’t any single step — it’s why the naive version fails and what each fix is actually solving.

How it works

Content first. The list is a CMS collection (“recognitions and awards”) with a handful of fields — an award category, the recognition type, some link CTA text, and a project thumbnail. Building it CMS-driven means the layout stays flexible: you style one collection item and every row inherits it.

The direction problem. The reveal has to feel right whether you enter a row from the top or the bottom. But Webflow’s hover interaction can’t tell you which edge the cursor crossed — a single hover animation would always slide the same way. Two fixes combine here. First, the text is built as three stacked wrappers (white, black, white) inside a row that’s exactly one wrapper tall with overflow: hidden, so only one shows at a time and there’s always a hidden neighbor to slide into view. Second, the row is split into two half-height, absolutely-positioned trigger divs — a top one and a bottom one — each with its own hover interaction. Now “enter from the top” and “enter from the bottom” are genuinely different events, and each plays the correct slide.

The image problem. A row with overflow: hidden would clip its own thumbnail, so each image is set to position: fixed — which lifts it out of the row’s context and pins it to the viewport corner instead — with pointer-events: none so it never blocks the rows underneath. Native interactions can scale one image from 0 to 1 on hover, and honestly you could stop there and ship a great result. But the reference does two things Webflow can’t: it stacks a new image on top each time (regardless of DOM order, so later rows don’t always win the z-index), and it can alternate between two images per row.

Where the code earns its keep. The workaround is elegant. Webflow’s click interaction fires a different animation on the first click versus the second — a free two-state toggle. So you wire two images and two click animations, then let a short script translate every hover into a click and raise the shown image’s z-index. Webflow still runs all the animation; the JavaScript only does the counting and the stacking it has no native hook for.

How to use it

  1. Create the CMS collection. Add a collection (e.g. Recognitions & Awards) with an option field for the award category, an option field for the recognition (nominee / awarded), a plain-text field for the link CTA copy, and an image field for the project thumbnail. Generate a few sample items to design against.

  2. Lay out the row. Add a collection list inside your section wrapper and bind it to the collection. On the collection item, set a bold display type (the tutorial uses font-size: 5.5vw, line-height: 1, all-caps) and position: relative — it’ll anchor absolutely-positioned children later. Inside, add a link block set to display: block and a custom color: inherit property to kill the default link blue.

  3. Build the three text wrappers. Inside the link block, add a text wrapper with 1vw padding on all sides and display: flex / justify-content: space-between holding two text elements (category + recognition). Duplicate it twice; give the middle copy a combo class with white text on a black background. This stacked white / black / white order is what makes the reveal work in both directions.

  4. Clip the row to one line. Compute one wrapper’s height (top + bottom padding + text height — here 1 + 1 + 5.5 = 7.5vw), set that as the item’s fixed height, and set overflow: hidden. Now only one wrapper shows and the others are clipped out of sight.

  5. Add the fixed preview image. Drop an image inside the link block, bind it to the thumbnail field, and set position: fixed, anchored bottom-right with 1vw offsets, width: 30vw, a landscape aspect ratio, object-fit: cover, pointer-events: none, and a high z-index (e.g. 1000).

  6. Split the row into two triggers. Add two absolutely-positioned div blocks inside the link block, each height: 50%, one pinned to the top half and one to the bottom. These are your direction-aware hover targets.

  7. Wire the direction-aware text animation. On the top trigger, add a Mouse-hover interaction. On hover, move the first text wrapper from -200% (initial state) to -100% on the Y axis, 0.7s, ease-out-quart; on hover-out, send it to -200%. Reuse that same “trigger on hover” animation for the bottom trigger, and give the bottom trigger its own hover-out that returns the wrapper to 0% — the opposite motion. This is the four-direction behavior, done with one shared animation and two hover-out variants.

  8. Scale the image on hover. Add a hover interaction to the list item that scales the bound image from 0 to 1 (0.7s, ease-out-quart) on hover, and back to 0 on hover-out. At this point you have a fully no-code version that already looks great.

  9. Prepare two images and a hidden trigger (for the JS version). To stack and alternate images, duplicate the image and give the two copies distinct combo classes. Add a tiny 1px absolutely-positioned div (the “images trigger”) and build a click interaction on it: the first click scales image one in (with a zero-duration reset action first), the second click scales image two in. Tag the collection item and this trigger with custom attributes (the tutorial uses fc-award-hover="item" and fc-award-hover="trigger-one") so the script can find them.

  10. Add the small script. In Page settings → Before </body>, a ~12-line script collects every item by its attribute, keeps a rising z-index counter, and on each item’s mouseenter it programmatically clicks the hidden trigger (firing the click animation Webflow already defined) and raises the just-shown image’s z-index so it stacks above the others, alternating between the two images per row. The exact code isn’t reproduced here — grab it from the cloneable so the attribute names and logic match your setup. Optionally, add a list-on-hover-out interaction on the collection list that scales the images back to 0 when the cursor leaves the whole list.

Resources

Frequently asked questions

How do you control which direction a hover animation plays in Webflow?
Webflow has no native way to detect hover direction. The trick is to place two absolutely-positioned, half-height div triggers inside the list item — one over the top half, one over the bottom — and attach a hover interaction to each. A shared 'trigger on hover' animation plus a separate 'hover out' animation per trigger then cover all four in/out directions cleanly.
How can you show only one line of text per item while animating the others in?
Set the list item height to exactly one text wrapper (its padding plus the text height) and set overflow to hidden. The extra stacked wrappers overflow and get clipped, so only one is ever visible, and the animation slides them up or down through that fixed window.
Why fix the preview image to the viewport instead of positioning it inside the item?
A list item with overflow: hidden would clip its own image. Setting the image to position: fixed removes it from that clipped context and anchors it to the viewport corner instead, so it stays visible. Add pointer-events: none so the floating image never blocks hovering the rows beneath it.
Can you animate two different images per list item in Webflow without code?
Partially. Webflow's click interaction plays one animation on the first click and a different one on the second, so mapping the first and second image to the first and second click gives you a two-image cycle. A few lines of JavaScript then turn each hover into a click and raise the shown image's z-index, so the previews stack in the right order.

← Back to the course