Sticky feature tabs
This is the feature showcase on roughly every product site, and the version people build has a setInterval for the advance and a separate CSS animation for the progress bar. Those two drift apart within a minute, and clicking a tab leaves the bar running on the old schedule. The fix is to stop thinking of them as two things: the progress tween is the timer. One gsap.to() takes six seconds to fill the bar and its onComplete advances the tab, so they can never disagree, a click can kill both with one call, and pausing is timer.pause(). The second half is knowing to stop it when nobody is looking — a ScrollTrigger onToggle, four lines, and the section is not silently cycling through four tabs while the visitor reads your hero.
How to build
Panel + script
Free plan gets you part of the way
The panel covers some of this. The rest needs the script, which needs a paid plan — build the panel half now, come back for the rest.
How the two routes work →Plugins
ScrollTrigger · Timeline
Build time
50 min including the panel mock-ups.
No-code?
Webflow's Tabs component plus its autoplay option gets you close, and it is a legitimate choice. What you cannot do in the panel is tie the progress bar to the same clock or pause on scroll-out — both of which are the reason to build it by hand.
Webflow structure
element · class · attribute<b>Section</b> <i>section_features</i> <b>Div</b> <i>features_layout</i> ← grid 0.85fr 1.15fr, align-items: start <b>Div</b> <i>features_sticky</i> ← position: sticky, top 12vh <b>Div</b> <i>tabs</i> <b>Button</b> <i>tab</i> <b>Text</b> <i>tab_no</i> <b>Div</b> <b>Heading H3</b> <b>Paragraph</b> <b>Div</b> <i>tab_progress</i> ← 2px track, absolute, bottom 0 <b>Div</b> <i>tab_progress-fill</i> ← scaleX(0), origin left <b>Button</b> <i>tab</i> … <b>Div</b> <i>features_stage</i> ← position: relative, fixed aspect-ratio, overflow hidden <b>Div</b> <i>features_panel</i> ← absolute inset 0, opacity 0 <b>Div</b> <i>features_panel</i> … (one per tab, same order)
Styles to set in the Designer
features_sticky | <code>position: sticky; top: 12vh</code>, and the grid must have <code>align-items: start</code> or the column stretches and never sticks. tab | A real <code><button></code>, full width, <code>position: relative</code> so the progress track can sit on its bottom edge. tab_progress-fill | <code>transform: scaleX(0); transform-origin: left</code>. This element is the clock — do not give it a CSS animation as well. features_stage | A <strong>fixed aspect-ratio</strong> and <code>overflow: hidden</code>. Panels are absolutely stacked, so the stage must have its own height or it collapses to nothing. features_panel | <code>position: absolute; inset: 0; opacity: 0</code>. Crossfade opacity — never <code>display</code>, which forces a layout pass and kills the stagger mid-flight.
Building it with the Interactions panel
no code, free plan- Use Webflow's native Tabs component for features_sticky / features_stage — it ships panel switching and an autoplay option directly in its element settings.
- Style tab_progress-fill as a plain CSS transition-based bar if you want something visual, or leave it off — the Tabs component's autoplay doesn't expose a bar you can bind to it.
What the panel still can’t do. Only custom code can close these gaps — there is no panel setting for any of them.
- The progress bar can't share the autoplay's clock — Tabs component autoplay is a black box with its own internal timer, so a panel-built bar is a second, separately-tuned animation that drifts out of sync with the actual tab change within a minute. That drift is exactly the bug this recipe exists to avoid.
- No pause-on-scroll-out — the Tabs component keeps autoplaying whether or not the section is on screen; stopping it via a scroll trigger needs the script's ScrollTrigger onToggle.
See “The script” below for the exact code.
Custom attributes
Element settings → Custom attributesCSS
Page settings → Inside <head> tagThe script
Page settings → Before </body> tag<!-- Page settings → Before </body> tag --> <script> document.addEventListener("DOMContentLoaded", () => { gsap.registerPlugin(ScrollTrigger); const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches; const tabs = gsap.utils.toArray(".tab"); const bars = gsap.utils.toArray(".tab_progress-fill"); const panels = gsap.utils.toArray(".features_panel"); const stage = document.querySelector(".features_stage"); const DURATION = 6; let current = -1; let timer = null; // the ONE tween that is also the clock function show(i) { if (i === current) return; current = i; tabs.forEach((t, n) => t.classList.toggle("is-on", n === i)); gsap.set(bars, { scaleX: 0 }); panels.forEach((p, n) => { gsap.to(p, { opacity: n === i ? 1 : 0, duration: 0.45, ease: "power2.out" }); if (n === i) { gsap.fromTo(p.children, { opacity: 0, y: 14 }, { opacity: 1, y: 0, duration: 0.5, ease: "power3.out", stagger: 0.05, overwrite: true }); } }); timer && timer.kill(); if (reduced) return; // no autoplay, click still works // The bar filling IS the countdown. One object, so a click can kill both // with a single call and they can never drift apart. timer = gsap.to(bars[i], { scaleX: 1, duration: DURATION, ease: "none", onComplete: () => show((i + 1) % tabs.length) }); } tabs.forEach((tab, i) => tab.addEventListener("click", () => show(i))); show(0); // Nobody is looking — stop burning frames and stop cycling through tabs // the visitor will never see. ScrollTrigger.create({ trigger: stage, start: "top bottom", end: "bottom top", onToggle: self => self.isActive ? timer && timer.resume() : timer && timer.pause() }); }); </script>
Gotchas that will cost you an hour
- Never use setInterval for the advance. It runs in a different clock from the progress animation, it keeps running in a background tab, and the two are visibly out of step inside a minute.
- Kill the old timer before starting a new one. Click through four tabs quickly without timer.kill() and you have four countdowns racing — the panel starts flipping on its own.
- Pause when off screen. Otherwise the section has cycled twice before the visitor reaches it, so the first tab they see is number three with no context.
- Six seconds, not three. A tab that advances before the copy can be read is worse than no autoplay. If the panel has more than a sentence in it, go to eight.
- Give the stage a fixed aspect-ratio. Absolutely-positioned panels contribute no height, so without one the stage collapses and the whole right-hand column vanishes.
- No autoplay under prefers-reduced-motion. Content that changes on its own is the exact thing that setting exists to prevent — keep the clicks, drop the timer.
Push it further
- Restart autoplay from the beginning after a manual click, but give it a longer duration — the visitor has shown interest in that panel.
- Drive the panel change from scroll instead of a timer: one ScrollTrigger per tab over a pinned section, and the section becomes a scroll-story.
- Add keyboard support with arrow keys and role="tablist". It is the difference between a tab component and four buttons that look like one.
Webflow Motion Lab — GSAP 3.15, all plugins free.
13
/ 30