Tab System with Autoplay Option

A GSAP-animated tab system with an optional autoplay mode. Each tab click or autoplay tick runs a coordinated timeline: the outgoing tab's details collapse and its progress bar resets, the incoming visual cross-fades in, and a progress bar scales from 0 to 1 over the autoplay duration before advancing to the next tab. Multiple independent instances per page are supported.

gsaptabsautoplayprogress baranimationinteractive

Setup — External Scripts

CDN — GSAP (add before </body>)
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.7/dist/gsap.min.js"></script>

Code

HTML
html
<div data-tabs-autoplay-duration="5000" data-tabs="wrapper" data-tabs-autoplay="true" class="tab-layout__wrap">
    <div class="tab-layout__col">
      <div class="tab-content__wrap">
        <div class="tab-content__inner">
          <div class="tab-content__top">
            <h1 class="tab-heading">Explore the perks of being a member</h1>
          </div>
          <div role="tablist" class="tab-content__bottom">
            <a role="tab" data-tabs="content-item" href="#" class="tab-content__item w-inline-block">
              <div class="tab-content__item-main">
                <div class="content-item__nr">
                  <div>01</div>
                </div>
                <h2 class="content-item__heading">Explore the vault</h2>
              </div>
              <div data-tabs="item-details" class="tab-content__item-detail">
                <div class="tab-description__spacer"></div>
                <p class="tab-description">The Vault is where everything lives. Organized into clear categories, it's designed to make browsing easy. Whether you're looking for a specific slider, animation, or utility, our quick-find search has you covered.</p>
                <div class="tab-description__spacer"></div>
              </div>
              <div class="tab-content__item-bottom">
                <div data-tabs="item-progress" class="tab-progress"></div>
              </div>
            </a>
            <a role="tab" data-tabs="content-item" href="#" class="tab-content__item w-inline-block">
              <div class="tab-content__item-main">
                <div class="content-item__nr">
                  <div>02</div>
                </div>
                <h2 class="content-item__heading">Learn from videos</h2>
              </div>
              <div data-tabs="item-details" class="tab-content__item-detail">
                <div class="tab-description__spacer"></div>
                <p class="tab-description">We also include videos that explain the concept, go deeper on the subject, or maybe might spark some new ideas for the resources that you're using.</p>
                <div class="tab-description__spacer"></div>
              </div>
              <div class="tab-content__item-bottom">
                <div data-tabs="item-progress" class="tab-progress"></div>
              </div>
            </a>
            <a role="tab" data-tabs="content-item" href="#" class="tab-content__item w-inline-block">
              <div class="tab-content__item-main">
                <div class="content-item__nr">
                  <div>03</div>
                </div>
                <h2 class="content-item__heading">Implement Osmo Basics</h2>
              </div>
              <div data-tabs="item-details" class="tab-content__item-detail">
                <div class="tab-description__spacer"></div>
                <p class="tab-description">These are the foundations you'll rely on for every award-worthy project. Master the basics, and the flashy stuff will actually have something solid to stand on.</p>
                <div class="tab-description__spacer"></div>
              </div>
              <div class="tab-content__item-bottom">
                <div data-tabs="item-progress" class="tab-progress"></div>
              </div>
            </a>
          </div>
        </div>
      </div>
    </div>
    <div class="tab-layout__col">
      <div aria-live="polite" role="region" class="tab-visual__wrap">
        <div id="tab1" data-tabs="visual-item" role="tabpanel" class="tab-visual__item active">
          <div class="tab-visual__inner"><img src="https://cdn.prod.website-files.com/679013b2e01832b21eba1b5b/679016ba86e862ccd6750213_tab-asset-vault.avif" loading="lazy" alt="" class="tab-image"></div>
        </div>
        <div id="tab2" data-tabs="visual-item" role="tabpanel" class="tab-visual__item">
          <div class="tab-visual__inner"><img src="https://cdn.prod.website-files.com/679013b2e01832b21eba1b5b/679016ba8f0f937c2b5b1d0f_tab-asset-videos.avif" loading="lazy" alt="" class="tab-image"></div>
        </div>
        <div id="tab3" data-tabs="visual-item" role="tabpanel" class="tab-visual__item">
          <div class="tab-visual__inner"><img src="https://cdn.prod.website-files.com/679013b2e01832b21eba1b5b/679016bab4910a3b7a9e0e64_tab-asset-basics.avif" loading="lazy" alt="" class="tab-image"></div>
        </div>
      </div>
    </div>
</div>
CSS
css
.tab-layout__wrap {
  z-index: 1;
  grid-row-gap: 3em;
  flex-flow: wrap;
  padding-left: 1em;
  padding-right: 1em;
  display: flex;
  position: relative;
}

.tab-layout__col {
  width: 50%;
  padding-left: .5em;
  padding-right: .5em;
}

.tab-content__inner {
  grid-column-gap: 3em;
  grid-row-gap: 3em;
  flex-flow: column;
  justify-content: space-between;
  align-items: flex-start;
  min-height: 100%;
  padding-top: 1em;
  padding-bottom: 0;
  padding-right: 2.5em;
  display: flex;
}

.tab-content__top {
  grid-column-gap: 2em;
  grid-row-gap: 2em;
  flex-flow: column;
  justify-content: flex-start;
  align-items: flex-start;
  display: flex;
}

.tab-heading {
  margin-top: 0;
  margin-bottom: 0;
  font-size: 3.5em;
  font-weight: 500;
  line-height: 1;
}

.tab-visual__wrap {
  aspect-ratio: 1.6;
  height: 50em;
  position: relative;
}

.tab-visual__item {
  visibility: hidden;
  justify-content: flex-start;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
}

.tab-visual__item.active {
  visibility: visible;
}

.tab-visual__inner {
  border: 1px solid #0003;
  border-radius: .5em;
  width: 100%;
  height: 100%;
  padding: .5em;
  overflow: hidden;
}

.tab-image {
  object-fit: cover;
  object-position: 0% 50%;
  border-radius: .25em;
  width: 100%;
  height: 100%;
  position: relative;
}

.tab-content__wrap {
  width: 100%;
  max-width: 36em;
  height: 100%;
  margin-left: auto;
  margin-right: 0;
}

.tab-content__bottom {
  flex-flow: column;
  justify-content: space-between;
  align-items: stretch;
  width: 100%;
  max-width: 30em;
  margin-top: 0;
  margin-bottom: 0;
  padding-left: 0;
  display: flex;
}

.tab-content__item {
  color: #131313;
  width: 100%;
  padding-top: 2em;
  padding-bottom: 2em;
  text-decoration: none;
  transition: opacity .25s;
  position: relative;
}

.tab-content__item-main {
  grid-column-gap: 2em;
  grid-row-gap: 2em;
  justify-content: flex-start;
  align-items: flex-start;
  width: 100%;
  display: flex;
}

.content-item__nr {
  color: #fff;
  background-color: #131313;
  border: 1px solid #131313;
  border-radius: 100em;
  justify-content: center;
  align-items: center;
  width: 2.5em;
  height: 2.5em;
  margin-top: .2em;
  font-family: RM Mono, Arial, sans-serif;
  font-size: .75em;
  font-weight: 400;
  transition: transform .4s cubic-bezier(.625, .05, 0, 1);
  display: flex;
}

.content-item__heading {
  margin-top: 0;
  margin-bottom: 0;
  font-size: 2em;
  font-weight: 500;
  line-height: 1;
}

.tab-content__item-detail {
  width: 100%;
  height: 0;
  padding-left: 4em;
  overflow: hidden;
}

.tab-description {
  margin-bottom: 0;
  font-size: 1em;
}

.tab-description__spacer {
  padding-top: 1em;
}

.tab-content__item-bottom {
  background-color: #0003;
  width: 100%;
  height: 1px;
  transition: background-color .2s;
  position: absolute;
  inset: auto 0% 0%;
}

.tab-progress {
  transform-origin: 0%;
  transform-style: preserve-3d;
  background-color: #ff4c24;
  width: 100%;
  height: 1px;
  transform: scale3d(0, 1, 1);
}

@media screen and (max-width: 991px) {
  .tab-layout__col {
    width: 100%;
    padding-left: 0;
    padding-right: 0;
  }

  .tab-content__inner {
    justify-content: space-between;
    align-items: stretch;
    padding: 0;
  }

  .tab-content__top {
    grid-column-gap: 1.5em;
    grid-row-gap: 1.5em;
  }

  .tab-visual__wrap {
    height: auto;
    padding-left: 0;
    padding-right: 0;
  }

  .tab-visual__item {
    overflow: hidden;
  }

  .tab-content__wrap {
    max-width: none;
    margin-left: 0;
  }
}

@media screen and (max-width: 767px) {
  .tab-layout__wrap {
    grid-row-gap: 2em;
  }

  .tab-heading {
    font-size: 2.8em;
  }

  .tab-visual__item {
    border-radius: .25em;
  }

  .tab-content__bottom {
    max-width: none;
  }

  .tab-content__item-main {
    grid-column-gap: 1.5em;
    grid-row-gap: 1.5em;
  }

  .content-item__nr {
    margin-top: -.2em;
  }

  .content-item__heading {
    font-size: 1.5em;
  }
}

@media screen and (max-width: 479px) {
  .tab-heading {
    font-size: 3em;
  }

  .tab-visual__inner {
    border-style: none;
    border-radius: .25em;
    padding: 0;
  }

  .tab-image {
    aspect-ratio: auto;
  }

  .tab-content__item {
    padding-top: 1.5em;
    padding-bottom: 1.5em;
  }

  .tab-content__item-main {
    grid-column-gap: 1em;
    grid-row-gap: 1em;
  }

  .content-item__nr {
    flex: none;
  }

  .content-item__heading {
    font-size: 1.5em;
  }

  .tab-content__item-detail {
    padding-left: 3em;
  }
}
JavaScript
javascript
function initTabSystem() {
  const wrappers = document.querySelectorAll('[data-tabs="wrapper"]');

  wrappers.forEach((wrapper) => {
    const contentItems = wrapper.querySelectorAll('[data-tabs="content-item"]');
    const visualItems = wrapper.querySelectorAll('[data-tabs="visual-item"]');

    const autoplay = wrapper.dataset.tabsAutoplay === "true";
    const autoplayDuration = parseInt(wrapper.dataset.tabsAutoplayDuration) || 5000;

    let activeContent = null;
    let activeVisual = null;
    let isAnimating = false;
    let progressBarTween = null;

    function startProgressBar(index) {
      if (progressBarTween) progressBarTween.kill();
      const bar = contentItems[index].querySelector('[data-tabs="item-progress"]');
      if (!bar) return;

      gsap.set(bar, { scaleX: 0, transformOrigin: "left center" });
      progressBarTween = gsap.to(bar, {
        scaleX: 1,
        duration: autoplayDuration / 1000,
        ease: "power1.inOut",
        onComplete: () => {
          if (!isAnimating) {
            const nextIndex = (index + 1) % contentItems.length;
            switchTab(nextIndex);
          }
        },
      });
    }

    function switchTab(index) {
      if (isAnimating || contentItems[index] === activeContent) return;

      isAnimating = true;
      if (progressBarTween) progressBarTween.kill();

      const outgoingContent = activeContent;
      const outgoingVisual = activeVisual;
      const outgoingBar = outgoingContent?.querySelector('[data-tabs="item-progress"]');

      const incomingContent = contentItems[index];
      const incomingVisual = visualItems[index];
      const incomingBar = incomingContent.querySelector('[data-tabs="item-progress"]');

      const tl = gsap.timeline({
        defaults: { duration: 0.65, ease: "power3" },
        onComplete: () => {
          activeContent = incomingContent;
          activeVisual = incomingVisual;
          isAnimating = false;
          if (autoplay) startProgressBar(index);
        },
      });

      if (outgoingContent) {
        outgoingContent.classList.remove("active");
        outgoingVisual?.classList.remove("active");
        tl.set(outgoingBar, { transformOrigin: "right center" })
          .to(outgoingBar, { scaleX: 0, duration: 0.3 }, 0)
          .to(outgoingVisual, { autoAlpha: 0, xPercent: 3 }, 0)
          .to(outgoingContent.querySelector('[data-tabs="item-details"]'), { height: 0 }, 0);
      }

      incomingContent.classList.add("active");
      incomingVisual.classList.add("active");
      tl.fromTo(incomingVisual, { autoAlpha: 0, xPercent: 3 }, { autoAlpha: 1, xPercent: 0 }, 0.3)
        .fromTo(incomingContent.querySelector('[data-tabs="item-details"]'), { height: 0 }, { height: "auto" }, 0)
        .set(incomingBar, { scaleX: 0, transformOrigin: "left center" }, 0);
    }

    switchTab(0);

    contentItems.forEach((item, i) =>
      item.addEventListener("click", () => {
        if (item === activeContent) return;
        switchTab(i);
      })
    );
  });
}

// Initialize Tab System with Autoplay Option
document.addEventListener('DOMContentLoaded', () => {
  initTabSystem();
});

Attributes

NameTypeDefaultDescription
data-tabs="wrapper"attributeRoot container for one tab instance. The script scopes all selectors to this element, so multiple independent instances on the same page are fully supported.
data-tabs-autoplayattribute ("true" | omit)Set to "true" to enable autoplay. Remove or set to any other value to disable. When enabled, startProgressBar() runs after each tab switch.
data-tabs-autoplay-durationattribute (number, milliseconds)Duration in milliseconds for each tab before auto-advancing. Defaults to 5000 ms if omitted. Controls both the progress bar tween duration and the autoplay interval.
data-tabs="content-item"attributeEach clickable tab trigger. Matched to visual items by DOM index — the 1st content-item activates the 1st visual-item.
data-tabs="item-details"attributeThe collapsible description panel inside each content item. Animated from height: 0 to height: auto on open and back to 0 on close.
data-tabs="item-progress"attributeThe progress indicator element. GSAP scales its X axis from 0 to 1 over the autoplay duration. Replace with any animation — circle stroke, border fill, counter, etc.
data-tabs="visual-item"attributeEach visual panel matched to its content item by index. Cross-fades in from xPercent: 3 when its tab becomes active and fades out to xPercent: 3 when it leaves.

Notes

  • Requires GSAP loaded via CDN before the script runs.
  • Content items and visual items are matched strictly by DOM index — keep them in the same order.
  • The isAnimating flag blocks a new switchTab() call while a transition is still running, preventing overlapping animations.
  • Clicking the currently active tab does nothing — the guard if (item === activeContent) return handles this.
  • progressBarTween is killed at the start of every switchTab() call to stop a running tween from triggering a second advance mid-transition.

Guide

Enabling and disabling autoplay

Add data-tabs-autoplay="true" to the wrapper to enable autoplay. Set data-tabs-autoplay-duration to control how long each tab stays active in milliseconds.

<!-- Autoplay enabled, 4 second interval -->
<div data-tabs="wrapper" data-tabs-autoplay="true" data-tabs-autoplay-duration="4000">

<!-- Autoplay disabled -->
<div data-tabs="wrapper">

Customising the progress indicator

The startProgressBar() function contains the GSAP tween for the progress element. Replace the scaleX animation with anything — a circle stroke, a counting timer, a filling border — as long as the onComplete callback calls switchTab(nextIndex).

Triggering on scroll entry

Wrap the initial switchTab(0) call in a ScrollTrigger so autoplay only starts when the user scrolls to the section.

ScrollTrigger.create({
  trigger: wrapper,
  start: "top center",
  once: true,
  onEnter: () => switchTab(0)
});

First-run guard

The outgoing animation block is wrapped in if (outgoingContent) — on the very first call there is no previous tab, so this prevents null-access warnings while still running the incoming animations.