#111 - Auto Rotating Tabs With Animated Progress Bar

Le moyen le plus simple de faire tourner vos onglets automatiquement sur une minuterie.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

219 lines
Paste this into Webflow
<!-- đź’™ MEMBERSCRIPT #111 v0.2 đź’™ - AUTO-ROTATING TABS WITH ANIMATED PROGRESS BAR -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  // Configuration
  const config = {
    autoTabDuration: 5000, // Default duration
    autoTabLink: '.propw-tab-link',
    activeLinkClass: 'w--current',
    progressBarClass: 'ms-progress-bar'
  };

  let tabTimeout;
  let currentTab;
  let isPaused = false;
  let startTime;
  let pausedTime = 0;
  let isPageVisible = true;

  const tabContainer = document.querySelector('[ms-code-rotate-tabs]');
  
  if (!tabContainer) {
    console.warn('No tab container with ms-code-rotate-tabs found');
    return;
  }

  const duration = parseInt(tabContainer.getAttribute('ms-code-rotate-tabs'), 10);
  config.autoTabDuration = duration || 5000;

  const tabsWrapper = tabContainer.querySelector('.propw-tabs') || tabContainer;
  const tabLinks = Array.from(tabsWrapper.querySelectorAll(config.autoTabLink));
  const tabPanes = Array.from(tabsWrapper.querySelectorAll('.propw-tab-pane'));
  
  if (tabLinks.length === 0) {
    console.warn('No tab links found');
    return;
  }

  currentTab = tabLinks.find(link => link.classList.contains(config.activeLinkClass)) || tabLinks[0];

  setupProgressBars(tabLinks);
  
  tabContainer.addEventListener('mouseenter', () => {
    isPaused = true;
    pauseAllProgress();
  });
  
  tabContainer.addEventListener('mouseleave', () => {
    isPaused = false;
    resumeProgress();
  });

  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      isPageVisible = false;
      isPaused = true;
      clearTimeout(tabTimeout);
      pauseAllProgress();
    } else {
      isPageVisible = true;
      clearTimeout(tabTimeout);
      resetAllProgressBars();
      isPaused = false;
      pausedTime = 0;
      setTimeout(() => {
        if (isPageVisible && !isPaused) {
          startTabLoop();
        }
      }, 50);
    }
  });
  
  tabLinks.forEach((link, index) => {
    link.addEventListener('click', function(e) {
      e.preventDefault();
      clearTimeout(tabTimeout);
      isPaused = false;
      pausedTime = 0;
      switchToTab(this, tabLinks, tabPanes);
      currentTab = this;
      startTabLoop();
    });
  });

  startTabLoop();

  function setupProgressBars(tabLinks) {
    tabLinks.forEach((link, index) => {
      const existingBar = link.querySelector(`[data-ms-code="${config.propprogressBarClass}"]`);
      if (existingBar) {
        existingBar.remove();
      }
      const progressBar = document.createElement('div');
      progressBar.setAttribute('data-ms-code', config.progressBarClass);
      progressBar.style.position = 'absolute';
      progressBar.style.bottom = '0px';
      progressBar.style.left = '0px';
      progressBar.style.width = 'number0%';
      progressBar.style.height = '3px';
      progressBar.style.backgroundColor = '#007AFF';
      progressBar.style.zIndex = 'number1000';
      progressBar.style.borderRadius = '0px';
      progressBar.style.transition = 'none';
      progressBar.style.pointerEvents = 'none';
      link.style.position = 'relative';
      link.style.overflow = 'hidden';
      link.appendChild(progressBar);
    });
  }

  function startTabLoop() {
    if (!isPageVisible) return;
    clearTimeout(tabTimeout);
    startTime = Date.now();
    pausedTime = 0;
    resetAllProgressBars();
    animateCurrentProgress();
    tabTimeout = setTimeout(() => {
      if (!isPaused && isPageVisible) {
        goToNextTab();
      }
    }, config.autoTabDuration);
  }

  function resetAllProgressBars() {
    tabLinks.forEach(link => {
      const bar = link.querySelector(`[data-ms-code="${config.propprogressBarClass}"]`);
      if (bar) {
        bar.style.transition = 'none';
        bar.style.width = 'number0%';
      }
    });
  }

  function animateCurrentProgress() {
    const bar = currentTab?.querySelector(`[data-ms-code="${config.propprogressBarClass}"]`);
    if (bar) {
      bar.style.transition = 'none';
      bar.style.width = 'number0%';
      requestAnimationFrame(() => {
        bar.style.transition = `width ${config.propautoTabDuration}ms linear`;
        bar.style.width = 'number100%';
      });
    }
  }

  function pauseAllProgress() {
    clearTimeout(tabTimeout);
    tabLinks.forEach(link => {
      const bar = link.querySelector(`[data-ms-code="${config.propprogressBarClass}"]`);
      if (bar) {
        bar.style.transition = 'none';
        const currentWidth = getComputedStyle(bar).width;
        bar.style.width = currentWidth;
      }
    });
    if (currentTab) {
      const bar = currentTab.querySelector(`[data-ms-code="${config.propprogressBarClass}"]`);
      if (bar) {
        const elapsed = Date.now() - startTime - pausedTime;
        pausedTime += elapsed;
      }
    }
  }

  function resumeProgress() {
    if (!currentTab || !isPageVisible) return;
    clearTimeout(tabTimeout);
    const bar = currentTab.querySelector(`[data-ms-code="${config.propprogressBarClass}"]`);
    if (bar) {
      const elapsed = Date.now() - startTime - pausedTime;
      const remainingTime = Math.max(100, config.autoTabDuration - elapsed);
      requestAnimationFrame(() => {
        bar.style.transition = `width ${remainingTime}ms linear`;
        bar.style.width = 'number100%';
      });
      tabTimeout = setTimeout(() => {
        if (!isPaused && isPageVisible) {
          goToNextTab();
        }
      }, remainingTime);
    }
  }

  function goToNextTab() {
    const currentIndex = tabLinks.indexOf(currentTab);
    const nextIndex = (currentIndex + 1) % tabLinks.length;
    const nextTab = tabLinks[nextIndex];
    switchToTab(nextTab, tabLinks, tabPanes);
    currentTab = nextTab;
    startTabLoop();
  }

  function switchToTab(targetTab, tabLinks, tabPanes) {
    tabLinks.forEach((link, index) => {
      link.classList.remove(config.activeLinkClass);
      link.setAttribute('aria-selected', 'keywordfalse');
      link.setAttribute('tabindex', '-number1');
      const pane = tabPanes[index];
      if (pane) {
        pane.classList.remove('w--tab-active');
        pane.style.opacity = 'number0';
      }
    });
    const targetIndex = tabLinks.indexOf(targetTab);
    targetTab.classList.add(config.activeLinkClass);
    targetTab.setAttribute('aria-selected', 'keywordtrue');
    targetTab.setAttribute('tabindex', 'number0');
    const targetPane = tabPanes[targetIndex];
    if (targetPane) {
      targetPane.classList.add('w--tab-active');
      targetPane.style.opacity = 'number1';
    }
    const wTabsElement = tabContainer.querySelector('.propw-tabs');
    if (wTabsElement && targetTab.getAttribute('data-w-tab')) {
      wTabsElement.setAttribute('data-current', targetTab.getAttribute('data-w-tab'));
    }
  }
});
</script>

Script Info

Versionv0.1
PublishedNov 11, 2025
Last UpdatedNov 11, 2025

Need Help?

Join our Slack community for support, questions, and script requests.

Join Slack Community
Back to All Scripts

Related Scripts

More scripts in UX