← 3D & depth

Rotating Cube

A real CSS 3D cube turns slowly in space, with your own content on each of its six faces. Drag it to give it a spin and it coasts to a stop before carrying on, or have it turn one face at a time and pause on each.

Plan
Design
Build
Test
Ship
Repeat

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-rotating-cube to a box with a height, holding six children: front, right, back, left, top and bottom.
  3. Style the faces however you like; the script sizes and places them.

Options

Add any of these attributes to the same element.

data-sizeEdge length in px. Default about half the box’s shorter side.
data-speedHow fast it turns, in degrees per second. Default 24.
data-tiltHow far the cube leans towards you, in degrees. Default -18.
data-stepMilliseconds: turn one face at a time, through all six, and pause this long on each.
data-drag="false"Don’t let people spin it by dragging.

With fewer than six children the faces repeat. With reduced motion the cube stands still.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Rotating Cube: a 3D cube turns slowly, with your content on each face.
    // Drag it to spin it by hand; let go and it carries on turning by itself.
    // Usage:
    //   <div data-rotating-cube>
    //     <div>One</div><div>Two</div><div>Three</div>
    //     <div>Four</div><div>Five</div><div>Six</div>
    //   </div>
    // The children become the faces: front, right, back, left, top, bottom.
    // With fewer than six they repeat.
    // Optional: data-size (edge length in px, default fits the box),
    // data-speed (degrees per second, default 24), data-tilt (degrees the cube
    // leans towards you, default -18), data-step (ms: turn one face at a time
    // and pause on each, instead of turning smoothly), data-drag="false".
    var DEFAULTS = { speed: 24, tilt: -18 };

    var css =
      '.rcb{position:relative;display:grid;place-items:center;perspective:900px;touch-action:pan-y;user-select:none;-webkit-user-select:none}' +
      '.rcb-drag{cursor:grab}.rcb-drag:active{cursor:grabbing}' +
      '.rcb-cube{position:relative;width:var(--rcb-s);height:var(--rcb-s);transform-style:preserve-3d}' +
      '.rcb-face{position:absolute;inset:0;display:grid;place-items:center;box-sizing:border-box;backface-visibility:hidden;-webkit-backface-visibility:hidden}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    // Where each face sits: [rotateY, rotateX].
    var FACES = [[0, 0], [90, 0], [180, 0], [-90, 0], [0, 90], [0, -90]];
    // The cube's turn that brings each face to the front, for data-step.
    var STOPS = [[0, 0], [-90, 0], [-180, 0], [-270, 0], [-360, -90], [-360, 90]];

    function reduced() { return window.matchMedia('(prefers-reduced-motion: reduce)').matches; }

    function setup(el) {
      var d = el.dataset;
      var kids = Array.from(el.children);
      if (!kids.length) return;
      el.classList.add('rcb');
      var cube = document.createElement('div');
      cube.className = 'rcb-cube';
      for (var i = 0; i < 6; i++) {
        var face = i < kids.length ? kids[i] : kids[i % kids.length].cloneNode(true);
        if (i >= kids.length) face.setAttribute('aria-hidden', 'true');
        face.classList.add('rcb-face');
        cube.appendChild(face);
      }
      el.appendChild(cube);
      var faces = Array.from(cube.children);
      var tilt = d.tilt !== undefined ? +d.tilt : DEFAULTS.tilt;
      var speed = d.speed !== undefined ? +d.speed : DEFAULTS.speed;
      var s = { ry: +d.step ? 0 : -30, rx: 0, vy: 0, dragging: false, size: 0 };

      function size() {
        var px = +d.size || Math.round(Math.min(el.clientWidth, el.clientHeight || el.clientWidth) * 0.52);
        if (px === s.size || !px) return;
        s.size = px;
        cube.style.setProperty('--rcb-s', px + 'px');
        faces.forEach(function (f, i) {
          f.style.transform = 'rotateY(' + FACES[i][0] + 'deg) rotateX(' + FACES[i][1] + 'deg) translateZ(' + px / 2 + 'px)';
        });
      }
      function draw() {
        cube.style.transform = 'rotateX(' + (tilt + s.rx) + 'deg) rotateY(' + s.ry + 'deg)';
      }
      size();
      draw();
      if (window.ResizeObserver) new ResizeObserver(size).observe(el);
      if (reduced()) return;

      var step = +d.step || 0, stop = 0, wait = step, last = 0, base = 0;

      // Drag to spin, with a little momentum when you let go.
      if (d.drag !== 'false') {
        el.classList.add('rcb-drag');
        var lastX = 0, lastT = 0;
        el.addEventListener('pointerdown', function (e) {
          s.dragging = true;
          s.vy = 0;
          lastX = e.clientX;
          lastT = performance.now();
          if (s.anim) { s.anim.pause(); s.anim = null; }
          el.setPointerCapture(e.pointerId);
        });
        el.addEventListener('pointermove', function (e) {
          if (!s.dragging) return;
          var now = performance.now(), dx = e.clientX - lastX;
          s.ry += dx * 0.5;
          s.vy = (dx * 0.5) / Math.max(8, now - lastT) * 1000;
          lastX = e.clientX;
          lastT = now;
          draw();
        });
        var end = function () {
          if (!s.dragging) return;
          s.dragging = false;
          if (!step) return;
          // Stepping: settle on the nearest side face, then carry on from there.
          s.vy = 0;
          var k = Math.round(s.ry / 90);
          stop = ((-k % 4) + 4) % 4;
          base = 90 * k + 90 * stop;
          s.anim = anime.animate(s, {
            ry: 90 * k, rx: 0, duration: 500, ease: 'out(3)',
            onUpdate: draw,
            onComplete: function () { s.anim = null; wait = step; }
          });
        };
        el.addEventListener('pointerup', end);
        el.addEventListener('pointercancel', end);
      }

      var timer = anime.createTimer({
        loop: true,
        duration: 1000,
        autoplay: false,
        onUpdate: function () {
          var now = performance.now();
          var dt = last ? Math.min(50, now - last) / 1000 : 0.016;
          last = now;
          if (s.dragging) return;
          if (s.vy) {
            // Coast after a drag, then settle back to the normal turn.
            s.ry += s.vy * dt;
            s.vy *= Math.pow(0.04, dt);
            if (Math.abs(s.vy) < speed) s.vy = 0;
            draw();
            return;
          }
          if (step) {
            if (s.anim) return;
            wait -= dt * 1000;
            if (wait > 0) return;
            stop++;
            if (stop === STOPS.length) { stop = 0; base -= 360; }
            var target = STOPS[stop];
            var ry = base + target[0];
            s.anim = anime.animate(s, {
              ry: ry, rx: target[1], duration: 1100, ease: 'inOut(3)',
              onUpdate: draw,
              onComplete: function () { s.anim = null; wait = step; }
            });
            return;
          }
          s.ry -= speed * dt;
          draw();
        }
      });
      new IntersectionObserver(function (entries) {
        if (entries[0].isIntersecting) { last = 0; timer.play(); } else timer.pause();
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-rotating-cube]').forEach(function (el) {
        if (el._rcb) return;
        el._rcb = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<div data-rotating-cube style="height: 320px">
  <div class="face">Plan</div>
  <div class="face">Design</div>
  <div class="face">Build</div>
  <div class="face">Test</div>
  <div class="face">Ship</div>
  <div class="face">Repeat</div>
</div>