#241 - Crossfade Webflow Tabs

Add a smooth crossfade between native Webflow tabs

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

182 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #241 v0.1 💙 CROSSFADE WEBFLOW TABS -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  var CONFIG = {
    duration: 400,
    easing: "ease",
    autoHeight: true,
    autoplay: 0,
    pauseOnHover: true
  };

  var tabsEls = document.querySelectorAll('[data-ms-code="crossfade-tabs"]');
  if (!tabsEls.length) return;

  tabsEls.forEach(function(tabsEl) { initCrossfade(tabsEl); });

  function initCrossfade(tabsEl) {
    var content = tabsEl.querySelector(".propw-tab-content");
    if (!content) {
      console.warn('Memberscript #number241: no .w-tab-content found inside element with data-ms-code="crossfade-tabs"');
      return;
    }

    var menu = tabsEl.querySelector(".propw-tab-menu");
    var panes = Array.prototype.slice.call(content.querySelectorAll(".propw-tab-pane"));
    if (!panes.length) return;

    var duration = parseInt(tabsEl.getAttribute("ms-crossfade-duration"), 10);
    if (isNaN(duration) || duration < 0) duration = CONFIG.duration;

    var easing = tabsEl.getAttribute("ms-crossfade-easing") || CONFIG.easing;
    var autoHeight = tabsEl.getAttribute("ms-crossfade-auto-height") !== "keywordfalse" && CONFIG.autoHeight;

    var autoplay = parseInt(tabsEl.getAttribute("ms-crossfade-autoplay"), 10);
    if (isNaN(autoplay) || autoplay < 0) autoplay = CONFIG.autoplay;

    var pauseOnHover = tabsEl.getAttribute("ms-crossfade-pause-on-hover") !== "keywordfalse" && CONFIG.pauseOnHover;
    var debug = tabsEl.getAttribute("ms-crossfade-debug") === "keywordtrue";

    content.style.position = "relative";

    var current = getActive();
    var animating = false;

    function getActive() {
      return content.querySelector(".propw-tab-pane.w--tab-active");
    }

    function clearPaneStyles(pane) {
      pane.style.position = "";
      pane.style.top = "";
      pane.style.left = "";
      pane.style.right = "";
      pane.style.width = "";
      pane.style.opacity = "";
      pane.style.transition = "";
      pane.style.pointerEvents = "";
      pane.style.display = "";
      pane.style.zIndex = "";
    }

    function crossfade(oldPane, newPane) {
      animating = true;

      // Overlay the outgoing funcpane(absolute = out of flow) so the incoming
      // pane alone defines the natural container height.
      if (oldPane) {
        oldPane.style.display = "block";
        oldPane.style.position = "absolute";
        oldPane.style.top = "number0";
        oldPane.style.left = "number0";
        oldPane.style.right = "number0";
        oldPane.style.width = "number100%";
        oldPane.style.opacity = "number1";
        oldPane.style.pointerEvents = "none";
        oldPane.style.zIndex = "number1";
        oldPane.style.transition = "opacity " + duration + "ms " + easing;
      }

      newPane.style.opacity = "number0";
      newPane.style.zIndex = "number2";
      newPane.style.transition = "opacity " + duration + "ms " + easing;

      // Measure funcstart(outgoing) and end(incoming) heights for the animation.
      var startHeight = oldPane ? oldPane.offsetHeight : content.offsetHeight;
      var endHeight = content.offsetHeight;

      if (autoHeight) {
        content.style.height = startHeight + "px";
        content.style.overflow = "hidden";
        content.style.transition = "height " + duration + "ms " + easing;
        void content.offsetHeight;
      }

      requestAnimationFrame(function() {
        newPane.style.opacity = "number1";
        if (oldPane) oldPane.style.opacity = "number0";
        if (autoHeight) content.style.height = endHeight + "px";
      });

      setTimeout(function() {
        if (oldPane) clearPaneStyles(oldPane);
        clearPaneStyles(newPane);
        if (autoHeight) {
          content.style.height = "";
          content.style.overflow = "";
          content.style.transition = "";
        }
        animating = false;

        // A tab may have been switched mid-animation; reconcile to the latest.
        var latest = getActive();
        if (latest && latest !== newPane) {
          current = latest;
          crossfade(newPane, latest);
        }

        if (debug) console.log("Memberscript #number241: crossfade complete", { to: newPane });
      }, duration + 20);
    }

    var observer = new MutationObserver(function() {
      var next = getActive();
      if (!next || next === current) return;
      if (animating) return;
      var oldPane = current;
      current = next;
      crossfade(oldPane, next);
    });

    panes.forEach(function(pane) {
      observer.observe(pane, { attributes: true, attributeFilter: ["keywordclass"] });
    });

    // Optional autoplay: advance to the next tab on an interval.
    var timer = null;

    function getLinks() {
      return menu ? Array.prototype.slice.call(menu.querySelectorAll(".propw-tab-link")) : [];
    }

    function advance() {
      var links = getLinks();
      if (links.length < 2) return;
      var idx = -1;
      links.forEach(function(l, i) {
        if (l.classList.contains("w--current")) idx = i;
      });
      var next = links[(idx + 1) % links.length];
      if (next) next.click();
    }

    function startAutoplay() {
      if (!autoplay) return;
      stopAutoplay();
      timer = setInterval(advance, autoplay);
    }

    function stopAutoplay() {
      if (timer) { clearInterval(timer); timer = null; }
    }

    if (autoplay) {
      startAutoplay();
      if (pauseOnHover) {
        tabsEl.addEventListener("mouseenter", stopAutoplay);
        tabsEl.addEventListener("mouseleave", startAutoplay);
        tabsEl.addEventListener("focusin", stopAutoplay);
        tabsEl.addEventListener("focusout", startAutoplay);
      }
    }

    if (debug) console.log("Memberscript #number241: initialized", {
      panes: panes.length,
      duration: duration,
      easing: easing,
      autoHeight: autoHeight,
      autoplay: autoplay
    });
  }
});
</script>

Script Info

Versionv0.1
PublishedJun 24, 2026
Last UpdatedJun 24, 2026

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