← Rotating words

Zoom Through Rotator

The word in your sentence rushes towards you, growing and blurring until it vanishes, as the next word zooms in from far away and snaps into focus.

Go further

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-zoom-through-rotator.

Options

Add any of these attributes to the same element.

data-holdHow long each word stays. Default 1800 ms.
data-durationTime for each swap. Default 800 ms.
data-scaleHow big the old word grows as it flies past. Default 1.8.
data-blurHow blurry words get in flight, in px. Default 6.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Zoom Through Rotator: the word in your sentence rushes towards you,
    // growing and blurring until it vanishes, as the next word zooms in from
    // far away and settles into place.
    // Usage: <h1>Go <span data-zoom-through-rotator="further, faster, beyond">further</span></h1>
    // Optional: data-hold (ms per word, default 1800), data-duration (ms per
    // swap, default 800), data-scale (how big the old word grows as it flies
    // past, default 1.8), data-blur (px, default 6).
    var DEFAULTS = { hold: 1800, duration: 800, scale: 1.8, blur: 6 };

    var css =
      '.ztr{display:inline-block;position:relative;white-space:nowrap;vertical-align:baseline;text-align:left}' +
      '.ztr-word{display:inline-block;white-space:nowrap;will-change:transform,opacity,filter}' +
      '.ztr-old{position:absolute;left:0;top:0;pointer-events:none}' +
      '.ztr-probe{position:absolute;left:0;top:0;visibility:hidden;white-space:nowrap}' +
      '.ztr-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) s.textContent = text;
      parent.appendChild(s);
      return s;
    }

    function setup(el) {
      var words = (el.getAttribute('data-zoom-through-rotator') || el.textContent)
        .split(',').map(function (w) { return w.trim(); }).filter(Boolean);
      el.classList.add('ztr');
      el.textContent = '';
      span('ztr-sr', el, words.join(', '));
      var out = span('ztr-word', el, words[0]);
      var probe = span('ztr-probe', el);
      out.setAttribute('aria-hidden', 'true');
      probe.setAttribute('aria-hidden', 'true');
      return { el: el, words: words, out: out, probe: probe, i: 0, w: 0, busy: false, visible: false, started: false };
    }

    function measure(s, text) {
      s.probe.textContent = text;
      return s.probe.offsetWidth;
    }

    function fit(s) {
      if (s.busy) return;
      s.w = measure(s, s.words[s.i]);
      s.el.style.width = s.w + 'px';
    }

    function swap(s, done) {
      var d = s.el.dataset;
      var t = +d.duration || DEFAULTS.duration;
      var big = +d.scale || DEFAULTS.scale;
      var blur = (d.blur !== undefined ? +d.blur : DEFAULTS.blur) + 'px';
      s.i = (s.i + 1) % s.words.length;
      var to = s.words[s.i];
      s.busy = true;

      // The old word flies at the viewer: it grows, blurs and fades out fast.
      var old = s.out;
      old.classList.add('ztr-old');
      anime.animate(old, {
        scale: [1, big], opacity: [1, 0], filter: ['blur(0px)', 'blur(' + blur + ')'],
        duration: t * 0.55, ease: 'in(2)',
        onComplete: function () { old.remove(); }
      });

      // The new word starts tiny and far away, then rushes into focus.
      s.out = span('ztr-word', s.el, to);
      s.out.setAttribute('aria-hidden', 'true');
      s.el.insertBefore(s.out, s.probe);
      anime.utils.set(s.out, { opacity: 0, scale: 0.25 });
      anime.animate(s.out, {
        scale: [0.25, 1], opacity: [0, 1], filter: ['blur(' + blur + ')', 'blur(0px)'],
        duration: t * 0.75, delay: t * 0.25, ease: 'out(4)'
      });

      anime.animate(s, {
        w: measure(s, to), duration: t, ease: 'inOut(3)',
        onUpdate: function () { s.el.style.width = s.w.toFixed(2) + 'px'; },
        onComplete: function () { s.busy = false; done(); }
      });
    }

    // Wait, then run fn, but only once the word is on screen.
    function wait(s, ms, fn) {
      setTimeout(function check() {
        if (s.visible && !document.hidden) fn();
        else setTimeout(check, 300);
      }, ms);
    }

    function start(s) {
      fit(s);
      if (document.fonts) document.fonts.ready.then(function () { fit(s); });
      window.addEventListener('resize', function () { fit(s); });
      var hold = +s.el.dataset.hold || DEFAULTS.hold;
      (function loop() {
        wait(s, hold, function () { swap(s, loop); });
      })();
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) {
          var s = e.target._ztr;
          s.visible = e.isIntersecting;
          if (s.visible && !s.started) { s.started = true; start(s); }
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-zoom-through-rotator]').forEach(function (el) {
        if (el._ztr) return;
        el._ztr = setup(el);
        if (!reduced && el._ztr.words.length > 1) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1>
  Go
  <span data-zoom-through-rotator="further, faster, beyond">further</span>
</h1>