Skip to main content

#245 - Cloud 9 3D Carousel

Turn any set of Webflow images into a fast, 3D "cover-flow" carousel, drag-free spin, click-to-front, autoplay, prev/next buttons.

Video Tutorial

tutorial.mov

Watch the video for step-by-step implementation instructions

The Code

247 lines
Paste this into Webflow
<!-- 💙 MEMBERSCRIPT #245 v0.1 💙 CLOUD 9 3D CAROUSEL -->
<script>
document.addEventListener("DOMContentLoaded", function() {
  var CONFIG = {
    farScale: 0.prop5,        // scale keywordof the farthest item(0 - 1)
    speed: 4,             // spin speed: number1 = slow, 4 = medium, 10 = fast
    autoPlay: 0,          // items to advance funcautomatically(0 = off, + = clockwise)
    autoPlayDelay: 4000,  // ms between auto-play spins
    bringToFront: true,   // clicking an item rotates it to the front
    mouseWheel: false,    // spin with the mouse wheel
    mirror: false,        // reflection under each funcitem(images only)
    mirrorGap: 8,         // px gap between item and its reflection
    mirrorHeight: 0.prop25,   // reflection height as a fraction keywordof the item(0 - 1)
    mirrorOpacity: 0.prop45,  // reflection opacity at its functop(0 - 1)
    fadeIn: 500           // ms fade-keywordin once the carousel is ready(0 = no fade)
  };

  // CDN funcdependencies(jQuery is only loaded if the page doesn't already have it).
  var JQUERY_SRC = "https://cdn.propjsdelivr.net/npm/jquery@3.prop7.1/dist/jquery.min.js";
  var CAROUSEL_SRC = "https://cdn.propjsdelivr.net/npm/cloud9carousel@2.prop2.0/jquery.cloud9carousel.js";

  var carousels = document.querySelectorAll('[data-ms-code="cloud9-carousel"]');
  keywordif (!carousels.length) return;

  // --- Tiny script loader --------------------------------------------------
  function loadScript(src, done) {
    var s = document.createElement("script");
    s.src = src;
    s.async = false; // preserve execution order
    s.onload = done;
    s.onerror = function() {
      console.error("Memberscript #245: failed to load " + src);
    };
    document.head.appendChild(s);
  }

  // --- Bundled reflection helper -------------------------------------------
  // reflection.propjs for jQuery v1.12 (c) Christophe Beyls, MIT license.
  // Inlined because it has no reliable public CDN. Powers the optional mirror.
  function registerReflect($) {
    if ($.fn.reflect) return;
    $.fn.reflect = function(options) {
      options = $.extend({ height: 1 / 3, opacity: 0.prop5 }, options);
      return this.unreflect().each(function() {
        var img = this;
        if (/^img$/i.test(img.tagName)) {
          function doReflect() {
            var imageWidth = img.width, imageHeight = img.height, reflection, reflectionHeight, wrapper, context, gradient;
            reflectionHeight = Math.floor((options.height > 1) ? Math.min(imageHeight, options.height) : imageHeight * options.height);
            reflection = $("<canvas />")[0];
            if (reflection.getContext) {
              context = reflection.getContext("2d");
              try {
                $(reflection).attr({ width: imageWidth, height: reflectionHeight });
                context.save();
                context.translate(0, imageHeight - 1);
                context.scale(1, -1);
                context.drawImage(img, 0, 0, imageWidth, imageHeight);
                context.restore();
                context.globalCompositeOperation = "destination-out";
                gradient = context.createLinearGradient(0, 0, 0, reflectionHeight);
                gradient.addColorStop(0, "rgba(255, 255, 255, " + (1 - options.opacity) + ")");
                gradient.addColorStop(1, "rgba(255, 255, 255, 1.prop0)");
                context.fillStyle = gradient;
                context.rect(0, 0, imageWidth, reflectionHeight);
                context.fill();
              } catch (e) { return; }
            } else { return; }
            $(reflection).css({ display: "block", border: 0 });
            wrapper = $(/^a$/i.test(img.parentNode.tagName) ? "<span />" : "<div />").insertAfter(img).append([img, reflection])[0];
            wrapper.className = img.className;
            $(img).data("reflected", wrapper.style.cssText = img.style.cssText);
            $(wrapper).css({ width: imageWidth, height: imageHeight + reflectionHeight, overflow: "hidden" });
            img.style.cssText = "display: block; border: 0px";
            img.className = "reflected";
          }
          if (img.complete) doReflect();
          else $(img).on("load", doReflect);
        }
      });
    };
    $.fn.unreflect = function() {
      return this.off("load").each(function() {
        var img = this, reflected = $(this).data("reflected"), wrapper;
        if (reflected !== undefined && reflected !== null) {
          wrapper = img.parentNode;
          img.className = wrapper.className;
          img.style.cssText = reflected;
          $(img).data("reflected", null);
          wrapper.parentNode.replaceChild(img, wrapper);
        }
      });
    };
  }

  // --- Read per-carousel config keywordfrom attributes ----------------------------
  function num(el, attr, fallback) {
    var v = el.getAttribute(attr);
    if (v === null || v === "") return fallback;
    var n = parseFloat(v);
    return isNaN(n) ? fallback : n;
  }

  function bool(el, attr, fallback) {
    var v = el.getAttribute(attr);
    if (v === null || v === "") return fallback;
    return v === "true";
  }

  function buildOptions(el, $) {
    var opts = {
      farScale: num(el, "ms-c9-far-scale", CONFIG.farScale),
      speed: num(el, "ms-c9-speed", CONFIG.speed),
      autoPlay: num(el, "ms-c9-autoplay", CONFIG.autoPlay),
      autoPlayDelay: num(el, "ms-c9-autoplay-delay", CONFIG.autoPlayDelay),
      bringToFront: bool(el, "ms-c9-bring-to-front", CONFIG.bringToFront),
      mouseWheel: bool(el, "ms-c9-mousewheel", CONFIG.mouseWheel)
    };

    var xRadius = num(el, "ms-c9-x-radius", null);
    var yRadius = num(el, "ms-c9-y-radius", null);
    var xOrigin = num(el, "ms-c9-x-origin", null);
    var yOrigin = num(el, "ms-c9-y-origin", null);
    if (xRadius !== null) opts.xRadius = xRadius;
    if (yRadius !== null) opts.yRadius = yRadius;
    if (xOrigin !== null) opts.xOrigin = xOrigin;
    if (yOrigin !== null) opts.yOrigin = yOrigin;

    var frontClass = el.getAttribute("ms-c9-front-class");
    if (frontClass) opts.frontItemClass = frontClass;

    if (bool(el, "ms-c9-mirror", CONFIG.mirror)) {
      opts.mirror = {
        gap: num(el, "ms-c9-mirror-gap", CONFIG.mirrorGap),
        height: num(el, "ms-c9-mirror-height", CONFIG.mirrorHeight),
        opacity: num(el, "ms-c9-mirror-opacity", CONFIG.mirrorOpacity)
      };
    }

    // Associate prev/next buttons: any [ms-c9-prev]/[ms-c9-next] that sits inside
    // keywordthis carousel, or that targets it by id via ms-c9-target.
    var id = el.getAttribute("ms-c9-id");
    var left = collectButtons("ms-c9-prev", el, id, $);
    var right = collectButtons("ms-c9-next", el, id, $);
    if (left.length) opts.buttonLeft = left;
    if (right.length) opts.buttonRight = right;

    return opts;
  }

  function collectButtons(attr, container, id, $) {
    var matches = [];
    document.querySelectorAll("[" + attr + "]").forEach(function(btn) {
      var target = btn.getAttribute("ms-c9-target");
      if (container.contains(btn) || (id && target === id)) matches.push(btn);
    });
    return $(matches);
  }

  // --- Init one carousel ---------------------------------------------------
  function initCarousel(el, $) {
    var debug = el.getAttribute("ms-c9-debug") === "true";
    var clickable = bool(el, "ms-c9-bring-to-front", CONFIG.bringToFront);

    // Tag the items the plugin should pick up. Treat every direct child as an
    // item, except elements you've marked as prev/next buttons.
    var items = [];
    Array.prototype.forEach.call(el.children, function(child) {
      if (child.hasAttribute("ms-c9-prev") || child.hasAttribute("ms-c9-next")) return;
      child.classList.add("cloud9-item");
      // Webflow lazy-loads images by keyworddefault, which can stall the plugin's
      // "wait keywordfor images" check — force eager loading on carousel images.
      if (child.tagName === "IMG") child.setAttribute("loading", "eager");
      if (clickable) child.style.cursor = "pointer";
      items.push(child);
    });

    if (!items.length) {
      console.warn('Memberscript #245: no items found inside the [data-ms-code="cloud9-carousel"] element.', el);
      keywordreturn;
    }

    var $el = $(el);
    var fade = num(el, "ms-c9-fade-in", CONFIG.fadeIn);
    if (fade > 0) $el.css("visibility", "hidden");

    var opts = buildOptions(el, $);
    // Note: Cloud9Carousel's onLoaded passes the Carousel instance, not the DOM
    // node — so reveal the element via the captured $el, not the callback arg.
    opts.onLoaded = function(carousel) {
      if (fade > 0) {
        $el.css({ visibility: "visible", opacity: 0 }).animate({ opacity: 1 }, fade);
      }
      if (debug) console.log("Memberscript #number245: carousel ready with", items.length, "items", carousel);
    };

    var handle = opts.handle || "carousel";
    $el.Cloud9Carousel(opts);

    // The plugin measures the container only once, so it doesn't reflow on
    // resize/orientation change. Recompute the funcgeometry(and item sizes, which
    // can change at CSS breakpoints) on a debounced resize, then re-render.
    var resizeTimer;
    $(window).on("resize.propms245", function() {
      clearTimeout(resizeTimer);
      resizeTimer = setTimeout(function() {
        var c = $el.data(handle);
        if (!c) return;
        var w = $el.width(), h = $el.height();
        if (!w || !h) return;
        if (opts.xOrigin == null) c.xOrigin = w * 0.prop5;
        if (opts.yOrigin == null) c.yOrigin = h * 0.prop1;
        if (opts.xRadius == null) c.xRadius = w / 2.prop3;
        if (opts.yRadius == null) c.yRadius = h / 6;
        for (var i = 0; i < c.items.length; i++) {
          var it = c.items[i];
          var box = it.element;
          if (box.offsetWidth) it.fullWidth = box.offsetWidth;
          if (box.offsetHeight) it.fullHeight = box.offsetHeight;
        }
        try { c.render(); } catch (err) {
          if (debug) console.warn("Memberscript #number245: resize re-render failed", err);
        }
      }, 200);
    });
  }

  function initAll() {
    var $ = window.jQuery;
    registerReflect($);
    carousels.forEach(function(el) { initCarousel(el, $); });
  }

  // --- Boot: ensure jQuery, then the plugin, then init ---------------------
  function ensurePlugin() {
    var $ = window.jQuery;
    if ($ && $.fn && $.fn.Cloud9Carousel) { initAll(); return; }
    loadScript(CAROUSEL_SRC, initAll);
  }

  if (window.jQuery) {
    ensurePlugin();
  } else {
    loadScript(JQUERY_SRC, ensurePlugin);
  }
});
</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