L1

·

State · autoplay

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.

Open the live demo →

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.

01

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)

02

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>&lt;button&gt;</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.

03

Building it with the Interactions panel

no code, free plan
  1. Use Webflow's native Tabs component for features_sticky / features_stage — it ships panel switching and an autoplay option directly in its element settings.
  2. 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.

04

Custom attributes

Element settings → Custom attributes

05

CSS

Page settings → Inside <head> tag

Inside <head> tag

Copy

06

The script

Page settings → Before </body> tag

Before </body> tag

Copy

<!-- 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>

07

Gotchas that will cost you an hour

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
08

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