← Rotating words

Scramble Word Rotator

One word in your sentence scrambles through random glyphs, then locks into the next word letter by letter, left to right, like a decoder cracking it. The word’s box eases to each new width as it goes.

Ship faster

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-scramble-word-rotator.

Options

Add any of these attributes to the same element.

data-holdHow long each word stays. Default 1800 ms.
data-durationTime for each scramble. Default 900 ms.
data-charsThe glyphs to scramble through. Default lowercase letters and a few symbols.

For a whole phrase that scrambles, see Text Scramble.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Scramble Word Rotator: one word in your sentence scrambles through random
    // glyphs, then locks into the next word letter by letter, left to right.
    // The word's box eases to each new width, so the sentence closes up around it.
    // Usage: <h1>Ship <span data-scramble-word-rotator="faster, smarter, safer">faster</span></h1>
    // Optional: data-hold (ms per word, default 1800), data-duration (ms per
    // scramble, default 900), data-chars (glyphs to scramble through).
    var DEFAULTS = { hold: 1800, duration: 900, chars: 'abcdefghijklmnopqrstuvwxyz#%&*+=?<>/' };

    var css =
      '.swr{display:inline-block;position:relative;white-space:nowrap;vertical-align:baseline;text-align:left}' +
      '.swr-busy{clip-path:inset(-.4em 0 -.4em -.2em)}' +
      '.swr-g{opacity:.45}' +
      '.swr-probe{position:absolute;left:0;top:0;visibility:hidden;white-space:nowrap}' +
      '.swr-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 escapeHtml(c) {
      return c.replace(/[&<>"]/g, function (m) { return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[m]; });
    }

    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-scramble-word-rotator') || el.textContent)
        .split(',').map(function (w) { return w.trim(); }).filter(Boolean);
      el.classList.add('swr');
      el.textContent = '';
      span('swr-sr', el, words.join(', '));
      var out = span('swr-word', el, words[0]);
      var probe = span('swr-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 chars = d.chars || DEFAULTS.chars;
      var from = s.words[s.i];
      s.i = (s.i + 1) % s.words.length;
      var to = s.words[s.i];
      var len = Math.max(from.length, to.length);
      var plan = [];
      for (var i = 0; i < len; i++) {
        // Letters start churning in a ripple and lock a random moment later,
        // so the new word resolves roughly left to right.
        var start = (i / len) * 0.3 + Math.random() * 0.1;
        plan.push({ from: from[i] || '', to: to[i] || '', start: start, end: Math.min(1, start + 0.3 + Math.random() * 0.3), glyph: '' });
      }
      s.busy = true;
      // Mid-scramble the glyphs can run wider than the box; trim them at its edge.
      s.el.classList.add('swr-busy');
      var w0 = s.w;
      var w1 = measure(s, to);
      var state = { p: 0 };
      anime.animate(state, {
        p: 1,
        duration: +d.duration || DEFAULTS.duration,
        ease: 'linear',
        onUpdate: function () {
          var p = state.p;
          // The width follows a smooth curve while the letters churn.
          var e = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2;
          s.w = w0 + (w1 - w0) * e;
          s.el.style.width = s.w.toFixed(2) + 'px';
          var html = '';
          plan.forEach(function (c) {
            if (p >= c.end) html += escapeHtml(c.to);
            else if (p >= c.start && (c.to || c.from) !== ' ') {
              if (!c.glyph || Math.random() < 0.4) c.glyph = chars.charAt(Math.floor(Math.random() * chars.length));
              html += '<span class="swr-g">' + escapeHtml(c.glyph) + '</span>';
            } else html += escapeHtml(c.from);
          });
          s.out.innerHTML = html;
        },
        onComplete: function () {
          s.out.textContent = to;
          s.w = w1;
          s.el.style.width = w1 + 'px';
          s.busy = false;
          s.el.classList.remove('swr-busy');
          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._swr;
          s.visible = e.isIntersecting;
          if (s.visible && !s.started) { s.started = true; start(s); }
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-scramble-word-rotator]').forEach(function (el) {
        if (el._swr) return;
        el._swr = setup(el);
        if (!reduced && el._swr.words.length > 1) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1>
  Ship
  <span data-scramble-word-rotator="faster, smarter, safer">faster</span>
</h1>