Learn how to build a dynamic testimonial hero in Webflow using CMS data, a custom JavaScript counter, and GSAP’s visual timeline.
I wanted this section to feel hand-crafted, with a layout that has its own character, even though every part of it is driven by Webflow CMS data.
It combines a slot-machine style counter, a manually arranged collage of member portraits, and a looping animation that continuously introduces new faces. Both the portraits and the counter are driven by CMS data, so the section updates automatically over time.
Getting all of that to work together turned out to be the interesting part. Webflow Collection Lists are designed to repeat the same layout for every item, while this design needs each portrait to have its own size and position.
I wanted to solve that while staying inside the Webflow Designer as much as possible. In this article I’ll walk through the three techniques that made it possible:
The counter is the first thing you see when the section scrolls into view, so it had to feel good: a number that spins up like the reels of a slot machine. The number comes from a live CMS entry that updates every month based on my course sales. And since there’s already a lot going on in the layout, I wanted the counter to play first and everything else to follow, but more on that animation later.
I can’t write JavaScript myself, so for a piece like this I let AI code it for me, and I’ve really come to enjoy working that way. A few years ago I’d have searched for some snippet online and adapted it where I could. Now it’s quick, and exactly how I want it. The only part that was tricky was the easing. I wanted the last digits to slow down and settle really softly at the end, so it would fit the calm feel of the whole page, and that exact feel took a lot of back and forth. That’s why I now handle these things differently: instead of describing tweaks in words, I ask the AI to build me a small configurator. A few sliders I can tune myself, until the easing is right.
Those settings were my feedback to the AI: they told it exactly how to generate the JS. And since I need count-up animations like this in client projects all the time, I turned it into a free tool you can open and use yourself: configure it, export the JS, and set one attribute on a text element, the script finds the number automatically. Counter-Up Animation Generator →
How the script worksI can read the code enough to follow it, but I can’t really tell whether the way it’s built is standard or whether there’s something clever worth sharing. So I asked the AI to point out what actually makes this script interesting, so I could pass the key ideas on to you. Here’s the short version.
The core idea: don’t count, slide. Instead of animating a value from 0 to its target, the script builds a vertical strip of digits for each place in the number:
overflow: hidden, exactly one line talltransform: translateY, and since the cell crops everything but one digit, you see the numbers spin past until the target landsNo counting logic, just a cropped, moving strip:
const e = 1 - Math.pow(1 - p, easePow); // ease-out: the part that took the most tuning
c.strip.style.transform =
'translateY(' + -(e * c.steps * dh.unit) + dh.css + ')';
Three smaller details are what actually made it feel right:
Count the revolutions from the right, not spread across the number. I wanted the left digit to lock in first and the right one to spin longest. If you spread the spins across the whole number, the jump from 952 to 1,000 re-sorts every place and the feel shifts. Counted from the right, the ones place always behaves the same, no matter how long the number gets:
const right = chars.slice(idx + 1).filter((c) => /\d/.test(c)).length;
const dist = total - 1 - right;
const f = dist / maxDist;
const revs = Math.max(1, Math.round(revLeft + (revRight - revLeft) * f));
Separate “how many spins” from “how long it takes.” A pure ease-out starts at full speed, which feels hectic. Splitting the two (revolutions vs. duration) is what gave real control over the feel, and it’s the pair of sliders I spent the most time on:
const steps = revs * 10 + final; // how far the digit travels
const myDur = Math.max(0.3, durRight - (maxDist - dist) * stagger); // how long it takes
Read the real line-height, don’t hard-code 1em. My heading uses a tight line-height: 0.9, so a 1em cell would be too tall and break the spacing below. The script reads the actual line height with getComputedStyle and moves the strip in that same unit:
const cs = getComputedStyle(el);
const lh = cs.lineHeight;
const px = lh === 'normal' ? parseFloat(cs.fontSize) * 1.2 : parseFloat(lh);
And finally the CMS binding: a tiny second script in the footer reads the real number and exposes it globally. The counter waits for that signal and for an IntersectionObserver to confirm it’s in view, so it runs exactly once, with the real number, at the right moment, which is what you see in the preview above.
The target layout
This is the part most people assume only works with custom code or some workaround, and it turned out to be a fun little challenge to solve.
Here’s where Webflow gets in the way. I have four portraits, at four different sizes, arranged like a collage, but they come from a CMS Collection List. Normally, if you want a manually-arranged grid, you would drop items into a CSS grid and give each one its own size and position. That works beautifully, but only outside of a collection list. A Webflow CMS Collection List currently doesn’t let you set items to individual positions.
By default, Webflow just outputs every item the same way: same size, same order, same repeated text underneath. But the design needs each portrait at its own size and position, and that’s the challenge.
The key realization was that I don’t need to reorder the items at all, I just need to style each position. In plain CSS you can target basically any item you want, which is really useful. Webflow only gives you four of these options directly in the Designer dropdown:
I wanted to see how far I could get with just those four, without writing any custom CSS. It turned out to be enough.
So each position gets its own width, plus a vertical shift on two of them so the tiles don’t sit in a straight row:
/* base width for every item, then override per position, all in em */
.user_grid-item { width: 10em; }
.user_grid-item:first-child { width: 5em; margin-top: 10em; }
.user_grid-item:nth-child(even) { width: 8em; margin-top: 10em; }
.user_grid-item:last-child { width: 6em; }
You might have noticed in the default output earlier that Webflow adds the same name and role label under every item, since the CMS repeats the content for each one. The same child selectors handle this too: with a small CSS embed I hide all the labels by default and only show one per collage, the third item’s label in the top collage and the second item’s label in the bottom one.
.user_grid-item-name { display: none; }
.user_grid-list .w-dyn-item:nth-child(3) .user_grid-item-name { display: block; }
Working with em units to scale the layout
A lot of people who follow my tutorials notice that I work with em here and there, but haven’t really brought it into their own workflow yet. This layout is a great example of why it’s worth it.
The principle: if you keep every value inside the user_grid-wrapper in em, you can scale the whole layout up or down with just the wrapper’s font-size. Every item resizes with it, no extra work.
Here’s how it’s set up:
container-type: inline-size, which you add as a custom property in Webflow, since it isn’t in the style panel to pick from. That makes the wrapper’s own width the reference for the em values inside itfont-size is set to 1cqw, one percent of that widthem is relative to the font-size, 1em now equals one percent of the wrapper width, so a 10em item is 10% of it, a 5em item is 5%, and so onuser_grid-wrapper itself sits inside my page container, which has a max-width.user_grid-wrapper {
container-type: inline-size;
font-size: 1cqw; /* 1% of the container width */
}
/* items in em then scale with the container automatically */
.user_grid-item { width: 10em; } /* = 10% of the container width */
All four CMS Collection items scale proportionally with it, together with the gradient shape behind them, which is exactly what you want for a graphic like this. Change the base font-size and the whole collage grows or shrinks as one piece.
This makes resizing the layout, especially for responsiveness, incredibly simple and fast. On mobile and tablet breakpoint I swap the cqw base for vw, and since the page container is already full width there, one vw behaves like one cqw. I set the collage to 2vw so it was bigger right away, and the layout still worked out nicely.
For this animation I used Webflow’s new visual timeline. It lets you build real GSAP animations right in the Designer, without writing any of the GSAP code yourself.
Before animating anything, I think about the order things should come in. It’s a section in the middle of the page, so the animation needs an initial spark to catch the visitor’s attention, but it also shouldn’t happen all at once, because the layout has a lot of small details and most of them get lost if everything moves at the same time. So the counter spins up first, and as it slows down the rest follows. A big part of the work was finding the sweet spot in the timing: how long the counter runs and when the collage items start swapping, so the gaps don’t feel too long and the animation keeps the viewer’s attention. All the elements in this section fade in slightly staggered.
The mask shape gradients also scale in a little, which gives them some extra movement.
One thing worth doing: I play the timeline on enter, pause on leave, and resume on scroll back, so it isn’t running in the background the whole time and weighing on performance.
The 3D member loop effect
The member loop is really the highlight of the whole animation. Since the section keeps filling with real new members over time, I wanted the faces to swap instead of just showing up once. And because they swap, I needed an animation both ways, one to bring a set in and one to send it out, which looks especially nice on the text as it staggers back out. The setup is simple: each Collection List sits in the section twice, and the second one is placed position: absolute right on top of the first. It skips ahead in the collection (skip four, skip eight) so it shows different faces, and then one list animates out while the other animates in.
The detail that makes it look premium is the 3D effect. A plain rotateX on its own looks flat: the card just tilts, but there’s no real depth to it. The fix is perspective, and the handy part is that you can set perspective and origin directly in the GSAP action-step UI, without setting up a 3D transform in the Webflow Designer first.
The settings I used for the incoming images:
The ease is a big part of the character here. I wanted a little bounce-back at the end, the kind of springy motion you see a lot in Apple’s iOS UI. It gives the swap a playful touch, and it works well with the calm gradients that just fade in softly. It’s something I plan to reuse across the page, and this section was a good place to see it work. The nice thing about Webflow’s GSAP timeline is that you pick the curve visually, with a built-in preview, so you can shape the elastic exactly how you want it.
The text uses its own effect, split line by line so it staggers in and out:
Two more details from this stage:
The set action. Since the two Collection Lists sit on top of each other, I keep the second one at opacity: 0 by default in the Webflow Designer, so both aren’t visible at once while I’m working and it stays out of my way. In the animation I then use a set action to switch it to full opacity in an instant, right at the moment before it animates in, while the other one is out. It’s not a tween, just an immediate state change at the exact right time.
Stagger from the outside in. The whole layout is built around the center, with the collage spread out toward the edges, so the swap looks best when it runs from the outer edges inward rather than left to right. Because each list has exactly four items, I start the stagger from item four, so the images swap from the outside toward the counter in the middle.
For the loop, my first instinct was to set the whole timeline to repeat infinitely, but that also reloops the one-time entrance fade-in, which I definitely didn’t want. The clean fix is to split it into two separate interactions: user-hero-beginning handles the entrance and plays once, and user-hero-collection-loop handles only the face-swap and repeats infinitely. The timing needs a hold between swaps, roughly four seconds, so the loop is really swap-out, swap-in, pause, repeat.
For reduced motion, the 3D swap loop is too much, but the gentle fade-in when the section scrolls into view is fine. This is where splitting the animation into two paid off again: with the intro fade and the loop as separate interactions, I could keep the fade for everyone and turn off just the loop. Webflow’s conditional playback handles it, so visitors who prefer reduced motion see the section fade in once and then stay still, without the faces swapping.
See it live
The section is live on my FORMBURG course page.
Credits| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | Creating an Interactive 3D Cluster with Three.js, TSL and Three Start | 0 | 10.33 | 12-08-2026 |
| 2 | Exploring Procedural Geometry with Three.js and WebGPU | 0 | 7.14 | 11-08-2026 |
| 3 | Run Rob Run: Building a Music-Reactive Goo with Three.js and WebGPU | 0 | 16.53 | 20-08-2026 |
| 4 | Relighting Images with Depth Maps and Three.js | 0 | 9.14 | 19-08-2026 |
| 5 | How to use video storytelling to connect with your audience | 0 | 7.35 | 26-05-2026 |
| 6 | How we brought agentic workflows to Cloud SIEM with the Datadog MCP Server | 0 | 8.36 | 17-07-2026 |
| 7 | What great analytics looks like when AI meets real behavior | 8 | 7 | 09-12-2025 |
| 8 | How to create CEO videos that build trust and inspire your audience | 0 | 5.73 | 10-03-2026 |
| 9 | Yoast x WTS Global: SEO is built in community | 0 | 10 | 19-05-2026 |
| 10 | How to create an evergreen webinar, plus top live event platforms | 0 | 8.03 | 08-06-2026 |