← Rotating words

3D Word Cube

One word in your sentence sits on a cube that rolls forward to show the next word. Faces dim as they turn away, like a real object catching the light.

Make it fast

How to use it

  1. Paste the snippet just before </head>.
  2. Wrap the changing word in a <span> and list every word, separated by commas, in data-word-cube.

Options

Add any of these attributes to the same element.

data-holdHow long each word stays. Default 1800 ms.
data-durationHow long each roll takes. Default 800 ms.
data-directionup (default): the next word rolls up from below. down: it drops in from above.

The cube is as wide as the longest word and sits on the sentence’s baseline, so nothing around it moves. It takes its font, size and colour from your text.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // 3D Word Cube: one word in a sentence sits on a cube that rolls forward to
    // show the next word. Faces dim as they turn away, like a lit object.
    // Usage: <h1>Make it <span data-word-cube="fast, simple, yours">fast</span></h1>
    // Optional: data-hold (ms per word), data-duration (ms per roll),
    // data-direction ("up" or "down").
    var DEFAULTS = { hold: 1800, duration: 800, ease: 'outBack(1.3)' };

    var css =
      '.wcx{display:inline-grid;position:relative;vertical-align:baseline;text-align:left;perspective:8em}' +
      '.wcx-size{grid-area:1/1;visibility:hidden;white-space:nowrap}' +
      '.wcx-cube{position:absolute;inset:0;transform-style:preserve-3d}' +
      '.wcx-face{position:absolute;inset:0;white-space:nowrap;backface-visibility:hidden;-webkit-backface-visibility:hidden}' +
      '.wcx-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function span(cls, parent, text) {
      var s = document.createElement('span');
      s.className = cls;
      if (text !== undefined) s.textContent = text;
      parent.appendChild(s);
      return s;
    }

    function build(el) {
      var words = (el.getAttribute('data-word-cube') || el.textContent)
        .split(',').map(function (w) { return w.trim(); }).filter(Boolean);
      el.classList.add('wcx');
      el.textContent = '';
      span('wcx-sr', el, words.join(', '));

      // Hidden copies of every word: the box takes the widest one and sits on the
      // sentence's baseline, so nothing around it shifts.
      words.forEach(function (w) { span('wcx-size', el, w).setAttribute('aria-hidden', 'true'); });

      var cube = span('wcx-cube', el);
      cube.setAttribute('aria-hidden', 'true');
      // "up": the next word rolls up from below; "down": it drops in from above.
      var dir = el.dataset.direction === 'down' ? -1 : 1;
      var faces = [];
      for (var i = 0; i < 4; i++) faces.push(span('wcx-face', cube, i ? '' : words[0]));

      var state = { el: el, words: words, faces: faces, cube: cube, dir: dir, step: 0, angle: 0, depth: 0 };
      var measure = function () { state.depth = el.offsetHeight / 2; render(state); };
      measure();
      if (window.ResizeObserver) new ResizeObserver(measure).observe(el);
      return state;
    }

    function render(s) {
      // Rotate about the cube's centre, half a line-height behind the text.
      s.cube.style.transform =
        'translateZ(' + -s.depth + 'px) rotateX(' + s.angle + 'deg)';
      s.faces.forEach(function (f, i) {
        var faceAngle = -s.dir * i * 90;
        f.style.transform = 'rotateX(' + faceAngle + 'deg) translateZ(' + s.depth + 'px)';
        var c = Math.cos(((faceAngle + s.angle) * Math.PI) / 180);
        f.style.opacity = c > 0.02 ? (0.2 + 0.8 * c * c).toFixed(3) : 0;
      });
    }

    function start(s) {
      if (s.words.length < 2) return;
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      var d = s.el.dataset;
      (function next() {
        s.step++;
        // Write the next word onto the face about to come into view.
        s.faces[s.step % 4].textContent = s.words[s.step % s.words.length];
        anime.animate(s, {
          angle: s.dir * s.step * 90,
          duration: +d.duration || DEFAULTS.duration,
          delay: +d.hold || DEFAULTS.hold,
          ease: DEFAULTS.ease,
          onUpdate: function () { render(s); },
          onComplete: next
        });
      })();
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          if (!e.isIntersecting) return;
          io.unobserve(e.target);
          start(e.target._wcx);
        });
      }, { threshold: 0.2 });
      document.querySelectorAll('[data-word-cube]').forEach(function (el) {
        if (el._wcx) return;
        el._wcx = build(el);
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1>
  Make it
  <span data-word-cube="fast, simple, yours">fast</span>
</h1>