# 3D Cards Tornado

> A helical 3D card orbit that auto-spins with drag, wheel, and touch control — ideal for immersive galleries.

**Definition:** 3D Cards Tornado is a sliders UI animation from collections.html. It is built with HTML, CSS, JavaScript, and GSAP, ships with a live preview, and is free to copy into personal and commercial projects.

## Facts
- Category: Sliders
- Stack: HTML, CSS, JavaScript, and GSAP
- Added: 2026-08-01
- Canonical page: https://html.bosconvertpro.com/collection/3d-cards-tornado
- Full-page preview: https://html.bosconvertpro.com/collection/3d-cards-tornado/full
- Live demo: https://html.bosconvertpro.com/demos/3d-cards-tornado/index.html
- Markdown mirror: https://html.bosconvertpro.com/collection/3d-cards-tornado/index.md
- License: https://html.bosconvertpro.com/terms

## How to use
1. Open https://html.bosconvertpro.com/collection/3d-cards-tornado/full to preview the animation in full screen.
2. Copy index.html, styles.css, and script.js from the code tabs on https://html.bosconvertpro.com/collection/3d-cards-tornado.
3. Paste the files into your project and adjust timing, easing, and content as needed.

## FAQ
### What is 3D Cards Tornado?

3D Cards Tornado is a sliders UI animation from collections.html. A helical 3D card orbit that auto-spins with drag, wheel, and touch control — ideal for immersive galleries.

### What is 3D Cards Tornado built with?

3D Cards Tornado is built with HTML, CSS, JavaScript, and GSAP. The source for every file is shown on the animation page and can be copied straight into your project.

### How do I use 3D Cards Tornado in my project?

Open the animation page (https://html.bosconvertpro.com/collection/3d-cards-tornado), preview it live, then copy index.html, styles.css, and script.js from the code tabs. Paste the files into your project and adjust timing, easing, and content as needed. A standalone demo runs at https://html.bosconvertpro.com/demos/3d-cards-tornado/index.html.

### Is 3D Cards Tornado free to use?

Yes. The 3D Cards Tornado demo code may be used in personal and commercial projects. You may not resell or redistribute the collection itself as a competing template pack. Full terms: https://html.bosconvertpro.com/terms.

## Source — index.html
````html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>3D Cards Tornado — GSAP</title>
    <link rel="preconnect" href="https://images.unsplash.com" />
    <link rel="preconnect" href="https://picsum.photos" />
    <link rel="preconnect" href="https://cdn.jsdelivr.net" />
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <main class="app">
      <div data-3d-tornado-init class="cards-tornado" aria-label="3D cards tornado gallery">
        <div class="cards-tornado__collection">
          <div data-3d-tornado-list class="cards-tornado__list">
            <!-- 1 · Mountain silhouette -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1506905925346-21bda4d32df4?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-mtn/720/900"
                  alt="Mountain landscape at golden hour"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 2 · Elegant portrait reflection -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-portrait/720/900"
                  alt="Elegant portrait in soft natural light"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 3 · Cyclist / sport portrait -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1517649763962-0c623066027b?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-sport/720/900"
                  alt="Athlete in motion under warm light"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 4 · Tennis court -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1554068865-24cecd4e34b8?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-tennis/720/900"
                  alt="Minimal tennis court with yellow balls"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 5 · Fresh fruit still life -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1610832958506-aa56368176cf?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-fruit/720/900"
                  alt="Fresh fruit still life on soft background"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 6 · Green plant stem -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1463936575829-25148e1db1b8?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-plant/720/900"
                  alt="Delicate green stem with soft buds"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 7 · Warm-tone woman portrait -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-woman/720/900"
                  alt="Serene portrait of a woman in warm tones"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 8 · Sailboat on ocean -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1505118380757-91f5f5632de0?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-sail/720/900"
                  alt="Sailboat on tranquil ocean under clear sky"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 9 · Eye close-up with freckles (hero match) -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-eye/720/900"
                  alt="Close-up portrait with freckles"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 10 · Lush green leaves -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1518531933037-91b2f5f229cc?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-leaf/720/900"
                  alt="Close-up of lush green leaves with texture"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 11 · Desert dunes -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1473580044384-7ba9967e16a0?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-dunes/720/900"
                  alt="Desert dunes glowing at sunset"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>

            <!-- 12 · Architecture facade -->
            <div data-3d-tornado-item class="cards-tornado__item">
              <div class="demo-card" draggable="false">
                <img
                  class="cover-image"
                  src="https://images.unsplash.com/photo-1487958449943-2429e8be8625?auto=format&fit=crop&w=720&q=80"
                  data-fallback="https://picsum.photos/seed/tornado-arch/720/900"
                  alt="Elegant modern building facade"
                  loading="lazy"
                  draggable="false"
                />
              </div>
            </div>
          </div>
        </div>
      </div>
    </main>

    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/Observer.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/ScrollTrigger.min.js"></script>
    <script src="script.js"></script>
  </body>
</html>
````

## Source — styles.css
````css
/* ─────────────────────────────────────────────
   3D Cards Tornado
   HTML / CSS / GSAP (Observer + ScrollTrigger)
───────────────────────────────────────────── */

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

html,
body {
  width: 100%;
  height: 100%;
}

body {
  font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
  background: #000;
  color: #fff;
  overflow: hidden;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

img {
  display: block;
  max-width: none;
  -webkit-user-drag: none;
  user-select: none;
}

.app {
  width: 100%;
  height: 100%;
  height: 100dvh;
  background: #000;
  overflow: hidden;
}

/* ── Tornado stage ─────────────────────────── */

.cards-tornado {
  cursor: grab;
  touch-action: none;
  width: 100%;
  height: 100%;
  height: 100dvh;
  position: relative;
  overflow: clip;
  background: #000;
  -webkit-user-select: none;
  user-select: none;
}

.cards-tornado:active {
  cursor: grabbing;
}

.cards-tornado__collection {
  width: 100%;
  height: 100%;
  position: relative;
}

.cards-tornado__list {
  /* em-based sizing drives card width via font-size */
  font-size: clamp(0.5em, 0.75vw, 1.5em);
  width: 100%;
  height: 100%;
  perspective: 75em;
  transform-style: preserve-3d;
  position: relative;
}

.cards-tornado__item {
  transform-style: preserve-3d;
  backface-visibility: visible;
  will-change: transform, filter;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* Start hidden until first render to avoid flash */
  opacity: 0;
}

/* ── Card ──────────────────────────────────── */

.demo-card {
  aspect-ratio: 4 / 5;
  pointer-events: none;
  -webkit-user-select: none;
  user-select: none;
  background-color: #1a1a1a;
  border-radius: 1em;
  width: 18em;
  position: relative;
  overflow: hidden;
  box-shadow:
    0 0.25em 1.5em rgba(0, 0, 0, 0.45),
    0 0 0 1px rgba(255, 255, 255, 0.04) inset;
}

.cover-image {
  object-fit: cover;
  border-radius: inherit;
  width: 100%;
  height: 100%;
  position: absolute;
  top: 0;
  left: 0;
  pointer-events: none;
}

/* ── Reduced motion ────────────────────────── */

@media (prefers-reduced-motion: reduce) {
  .cards-tornado__item {
    transition: none !important;
  }
}

/* ── Small screens — slightly tighter cards ── */

@media (max-width: 640px) {
  .cards-tornado__list {
    font-size: clamp(0.45em, 2.2vw, 0.85em);
  }
}
````

## Source — script.js
````javascript
/**
 * 3D Cards Tornado
 * GSAP + Observer + ScrollTrigger
 *
 * Cards form a helical 3D orbit (tornado). Auto-rotates forever.
 * Wheel / drag / touch accelerate and reverse the spin.
 */

(() => {
  "use strict";

  if (typeof gsap === "undefined") {
    console.error("[3D Tornado] GSAP failed to load from CDN.");
    return;
  }

  gsap.registerPlugin(Observer, ScrollTrigger);
  gsap.config({ force3D: true, nullTargetWarn: false });
  gsap.ticker.lagSmoothing(500, 33);

  /* ── Image fallbacks (Unsplash → Picsum) ── */
  document.querySelectorAll("img.cover-image[data-fallback]").forEach((img) => {
    img.addEventListener(
      "error",
      function onErr() {
        const fb = img.getAttribute("data-fallback");
        if (fb && img.src !== fb) img.src = fb;
        img.removeEventListener("error", onErr);
      },
      { once: true }
    );
  });

  function init3DCardsTornado() {
    const containers = gsap.utils.toArray("[data-3d-tornado-init]");

    /* Tunables */
    const rotationAngle = 30; // degrees between cards on the orbit
    const cardYSpacing = 0.3; // vertical offset as fraction of card height
    const edgeOffset = 2; // how far past viewport edge fade starts
    const orbitDepth = 35; // radius of tornado (in em)
    const autoSpeed = 0.00325; // idle spin speed
    const scrollSpeed = 0.015; // input → velocity scale
    const dragMultiplier = 5; // pointer/touch sensitivity boost
    const scrollEase = 0.1; // velocity lerp toward autoSpeed
    const maxSpeed = 0.2; // clamp peak velocity
    const edgeScale = 0.5; // distance over which edge fade runs
    const edgeEase = gsap.parseEase("power2.inOut");
    const minScale = 1; // scale floor for far cards (1 = no shrink by depth)
    const backDarkness = 0.75; // max darkening for cards behind the camera
    const backBlur = 0.5; // max blur (em) for rear cards

    containers.forEach((container) => {
      const list = container.querySelector("[data-3d-tornado-list]");
      if (!list) return;

      /* Snapshot originals before rebuild clones them */
      const originalCards = gsap.utils
        .toArray("[data-3d-tornado-item]", list)
        .map((card) => card.cloneNode(true));

      if (!originalCards.length) return;

      let inputObserver = null;
      let resizeTimer = null;

      const state = {
        amount: 0,
        progress: 0,
        velocity: autoSpeed,
        direction: 1,
        cardHeight: 0,
        cardGap: 0,
        em: 16,
        isActive: false,
        cards: [],
      };

      function getCardAmount() {
        const containerHalfHeight = container.offsetHeight * 0.5;
        const edgeOffsetDistance = state.cardHeight * edgeOffset;
        const fadeDistance = state.cardHeight * edgeScale;
        const neededDistance =
          containerHalfHeight + edgeOffsetDistance + fadeDistance;
        const cardsPerSide = Math.ceil(neededDistance / state.cardGap) + 1;
        const neededAmount = cardsPerSide * 2 + 1;
        const batchCount = Math.ceil(neededAmount / originalCards.length);
        return originalCards.length * Math.max(1, batchCount);
      }

      function buildCards() {
        list.innerHTML = "";

        /* Measure one card for height / em */
        const measureCard = originalCards[0].cloneNode(true);
        list.appendChild(measureCard);
        state.cardHeight = measureCard.offsetHeight || 280;
        state.cardGap = state.cardHeight * cardYSpacing;
        state.em =
          parseFloat(getComputedStyle(measureCard).fontSize) ||
          parseFloat(getComputedStyle(list).fontSize) ||
          16;
        state.amount = getCardAmount();

        list.innerHTML = "";

        for (let i = 0; i < state.amount; i++) {
          const card = originalCards[i % originalCards.length].cloneNode(true);
          card.dataset.index = String(i);

          /* Re-wire image fallbacks on clones */
          card.querySelectorAll("img.cover-image[data-fallback]").forEach((img) => {
            img.addEventListener(
              "error",
              function onErr() {
                const fb = img.getAttribute("data-fallback");
                if (fb && img.src !== fb) img.src = fb;
                img.removeEventListener("error", onErr);
              },
              { once: true }
            );
          });

          list.appendChild(card);
        }

        state.cards = gsap.utils.toArray("[data-3d-tornado-item]", list);
      }

      /** Scale factor that fades cards near vertical edges of the viewport */
      function getEdgeScale(y) {
        const containerHalfHeight = container.offsetHeight * 0.5;
        const edgeOffsetDistance = state.cardHeight * edgeOffset;
        const fadeDistance = state.cardHeight * edgeScale;
        const distanceFromCenter = Math.abs(y);
        const fadeStart = containerHalfHeight + edgeOffsetDistance;
        const progress = gsap.utils.clamp(
          0,
          1,
          (fadeStart - distanceFromCenter) / fadeDistance
        );
        return edgeEase(progress);
      }

      function render() {
        const radius = orbitDepth * state.em;

        state.cards.forEach((card) => {
          const startIndex = parseFloat(card.dataset.index);
          const loopIndex =
            (((startIndex + state.progress) % state.amount) + state.amount) %
            state.amount;
          /* Center the loop around 0 so cards sit above & below the middle */
          const index =
            loopIndex > state.amount * 0.5
              ? loopIndex - state.amount
              : loopIndex;

          const angleDeg = index * rotationAngle;
          const angleRad = (angleDeg * Math.PI) / 180;

          /* 0 at center of chain, 1 at farthest ends — used for z-index */
          const center = 1 - Math.min(Math.abs(index) / (state.amount * 0.5), 1);
          const y = index * state.cardGap;

          const baseScale = minScale + center * (1 - minScale);
          const scale = baseScale * getEdgeScale(y);

          /* Cards on the far side of the orbit (cos ≈ -1) get dark + blur */
          const backAmount = gsap.utils.clamp(0, 1, (1 - Math.cos(angleRad)) * 0.5);
          const brightness = 1 - backAmount * backDarkness;
          const blur = backAmount * backBlur;

          gsap.set(card, {
            xPercent: -50,
            yPercent: -50,
            x: Math.sin(angleRad) * radius,
            y,
            z: (Math.cos(angleRad) - 1) * radius,
            rotateY: angleDeg,
            scale,
            filter: `brightness(${brightness}) blur(${blur}em)`,
            autoAlpha: 1,
            zIndex: Math.round(center * 1000),
            force3D: true,
          });
        });
      }

      function tick() {
        if (!state.isActive) return;
        const targetVelocity = autoSpeed * state.direction;
        state.velocity = gsap.utils.interpolate(
          state.velocity,
          targetVelocity,
          scrollEase
        );
        state.progress += state.velocity;
        render();
      }

      function handleInput(self) {
        if (!state.isActive) return;

        const isWheel = self.event && self.event.type === "wheel";
        let delta;

        if (isWheel) {
          delta = self.deltaY;
        } else {
          /* Prefer dominant axis for drag / touch */
          delta =
            Math.abs(self.deltaX) > Math.abs(self.deltaY)
              ? self.deltaX * dragMultiplier
              : self.deltaY * dragMultiplier;
        }

        if (!delta) return;

        state.direction = delta > 0 ? 1 : -1;
        state.velocity += (delta * scrollSpeed) / 100;
        state.velocity = gsap.utils.clamp(-maxSpeed, maxSpeed, state.velocity);
      }

      function setActive(isActive) {
        state.isActive = isActive;
        if (!inputObserver) return;
        if (isActive) {
          inputObserver.enable();
        } else {
          inputObserver.disable();
        }
      }

      function rebuild() {
        buildCards();
        render();
      }

      rebuild();

      inputObserver = Observer.create({
        target: window,
        type: "wheel,touch,pointer",
        preventDefault: false,
        lockAxis: true,
        onChange: handleInput,
        onPress: () => {
          container.style.cursor = "grabbing";
        },
        onRelease: () => {
          container.style.cursor = "grab";
        },
      });

      ScrollTrigger.create({
        trigger: container,
        start: "top bottom",
        end: "bottom top",
        onEnter: () => setActive(true),
        onEnterBack: () => setActive(true),
        onLeave: () => setActive(false),
        onLeaveBack: () => setActive(false),
      });

      setActive(ScrollTrigger.isInViewport(container));
      gsap.ticker.add(tick);

      window.addEventListener("resize", () => {
        clearTimeout(resizeTimer);
        resizeTimer = setTimeout(() => {
          rebuild();
          ScrollTrigger.refresh();
        }, 150);
      });
    });
  }

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", init3DCardsTornado);
  } else {
    init3DCardsTornado();
  }
})();
````
