Accelerating Globe on Scroll

A CSS wireframe globe whose rotation speed scales with scroll velocity. Eight mirrored ring segments animate in a looping GSAP timeline; when the user scrolls, the timeline's timeScale spikes based on scroll speed and eases back to 1x after scrolling stops.

gsapscrollanimationglobegimmick

Setup — External Scripts

External Scripts
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.13.0/dist/gsap.min.js"></script>

Code

HTML
html
<div data-accelerating-globe="" class="globe">
  <div class="globe__before"></div>
  <div class="globe__back">
    <div class="globe__back-circle"></div>
    <div class="globe__back-circle is--1"></div>
    <div class="globe__back-circle is--2"></div>
    <div class="globe__back-circle is--3"></div>
    <div class="globe__back-circle is--4"></div>
    <div class="globe__back-circle is--5"></div>
  </div>
  <div class="globe__front">
    <div class="globe__mirror">
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
    </div>
    <div class="globe__mirror is--duplicate">
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
      <div data-accelerating-globe-circle="" class="globe__circle">
        <div class="globe__circle-inner"></div>
      </div>
    </div>
  </div>
</div>
CSS
css
.globe {
  color: #d500ff;
  justify-content: center;
  align-items: center;
  width: 37.5vw;
  display: flex;
  position: relative;
}

.globe__before {
  padding-top: 100%;
}

.globe__back {
  border-radius: 50%;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 100%;
  display: flex;
  position: absolute;
  overflow: hidden;
}

.globe__back-circle {
  border: 1px solid;
  border-radius: 50%;
  width: 100%;
  height: 100%;
  position: absolute;
}

.globe__back-circle.is--1 {
  width: 50%;
  height: 16%;
  top: 0%;
}

.globe__back-circle.is--2 {
  width: 87.5%;
  height: 24%;
  top: 14%;
}

.globe__back-circle.is--3 {
  border-radius: 50%;
  width: 100%;
  height: 28%;
  top: 36%;
}

.globe__back-circle.is--4 {
  width: 87.5%;
  height: 24%;
  top: 62%;
}

.globe__back-circle.is--5 {
  width: 50%;
  height: 16%;
  top: 84%;
}

.globe__front,
.globe__mirror {
  width: 100%;
  height: 100%;
  position: absolute;
}

.globe__mirror.is--duplicate {
  transform: scaleX(-1);
}

.globe__circle {
  width: 50%;
  height: 100%;
  position: absolute;
  left: auto;
  right: 50%;
  overflow: hidden;
}

.globe__circle-inner {
  border: 1px solid;
  border-radius: 50%;
  width: 200%;
  height: 100%;
  position: absolute;
}
JavaScript
javascript
function initAcceleratingGlobe() {
  document.querySelectorAll('[data-accelerating-globe]').forEach(function(globe) {
    const circles = globe.querySelectorAll('[data-accelerating-globe-circle]');
    if (circles.length < 8) return;

    const tl = gsap.timeline({
      repeat: -1,
      defaults: { duration: 1, ease: "none" },
    });

    const widths = [
      ["50%",              "37.5%"           ],
      ["37.5%",            "25%"             ],
      ["25%",              "12.5%"           ],
      ["calc(12.5% + 1px)","calc(0% + 1px)" ],
      ["calc(0% + 1px)",   "calc(12.5% + 1px)"],
      ["12.5%",            "25%"             ],
      ["25%",              "37.5%"           ],
      ["37.5%",            "50%"             ],
    ];

    circles.forEach((el, i) => {
      const [fromW, toW] = widths[i];
      tl.fromTo(el, { width: fromW }, { width: toW }, i === 0 ? 0 : "<");
    });

    let lastY = window.scrollY;
    let lastT = performance.now();
    let stopTimeout;

    function onScroll() {
      const now = performance.now();
      const dy = window.scrollY - lastY;
      const dt = now - lastT;
      lastY = window.scrollY;
      lastT = now;

      const velocity = dt > 0 ? (dy / dt) * 1000 : 0; // px/s
      const boost = Math.abs(velocity * 0.005);
      const targetScale = boost + 1;

      tl.timeScale(targetScale);

      clearTimeout(stopTimeout);
      stopTimeout = setTimeout(() => {
        gsap.to(tl, {
          timeScale: 1,
          duration: 0.6,
          ease: "power2.out",
          overwrite: true,
        });
      }, 100);
    }

    window.addEventListener("scroll", onScroll, { passive: true });
  });
}

document.addEventListener('DOMContentLoaded', function() {
  initAcceleratingGlobe();
});

Attributes

NameTypeDefaultDescription
data-accelerating-globeattributeRoot element of each globe instance. The script queries all elements with this attribute, so multiple independent globes on the same page are supported.
data-accelerating-globe-circleattributeEach animated ring segment. Requires exactly 8 (4 per .globe__mirror). The script skips the globe if fewer than 8 are found.

Notes

  • Requires GSAP loaded via CDN before the script runs.
  • At least 8 [data-accelerating-globe-circle] elements are required — the script skips any globe with fewer.
  • Multiple globes on the same page are supported; each gets its own timeline and scroll listener.
  • The scroll listener uses { passive: true } for performance.
  • Adjust the 0.005 multiplier in the velocity → timeScale formula to make the speed boost more or less sensitive to scroll.

Guide

Ring animation

The 8 [data-accelerating-globe-circle] elements animate their width in sequence within a single repeating timeline. All 8 tweens are placed at position 0 (they run in parallel), cycling through widths that simulate a meridian sweeping across the sphere surface.

Scroll velocity to timeScale

On each scroll event, the script calculates velocity in px/s from the delta scroll and delta time. The timeScale is set to 1 + |velocity| × 0.005. After 100ms of no scrolling, it tweens back to 1x with a power2.out ease.

const velocity = dt > 0 ? (dy / dt) * 1000 : 0; // px/s
const boost = Math.abs(velocity * 0.005);
tl.timeScale(boost + 1);

Mirrored structure

The .globe__front contains two .globe__mirror wrappers. The second (.is--duplicate) is flipped horizontally with scaleX(-1). Together they create the illusion of a full sphere from 4 half-circle rings per side.

Static back layer

The .globe__back contains 6 ellipses positioned at fixed latitude lines (the full equator plus 5 latitude bands). These are purely CSS and do not animate — they provide the grid reference that makes the rotating front rings read as a 3D sphere.