← Rotating words

Emoji Word Rotator

Every word brings its own emoji. As the word slides up to the next, the emoji beside it turns edge-on and flips round, landing with a little pop as its new face.

Made with love

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-emoji-word-rotator.
  3. Start each word with its emoji and a space, like ☕ coffee, or list the emoji separately in data-emojis.

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-positionbefore (default) puts the emoji in front of the word; after puts it behind.
data-emojisThe emoji, separated by commas, if you’d rather keep them out of the word list.
1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Emoji Word Rotator: each new word arrives with its own emoji, which flips
    // in beside it like a coin turning over while the word slides up into place.
    // Usage: <h1>Made with <span data-emoji-word-rotator="❤️ love, ☕ coffee, ✨ magic">love</span></h1>
    //   Start each word with its emoji and a space. Or list the emoji separately
    //   in data-emojis="❤️, ☕, ✨".
    // Optional: data-hold (ms per word, default 1800), data-duration (ms per
    // swap, default 800), data-position ("before", the default, or "after").
    var DEFAULTS = { hold: 1800, duration: 800, position: 'before' };

    var css =
      '.emw{display:inline-block;position:relative;white-space:nowrap;vertical-align:baseline;text-align:left}' +
      '.emw-emoji{display:inline-block;width:1.25em;text-align:center;perspective:4em}' +
      '.emw-face{display:inline-block;font-style:normal;color:initial;will-change:transform}' +
      '.emw-clip{display:inline-block;position:relative;box-sizing:content-box;clip-path:inset(-.04em -.3em -.12em -.3em)}' +
      '.emw-word{display:inline-block}' +
      '.emw-old{position:absolute;left:0;top:0}' +
      '.emw-probe{position:absolute;left:0;top:0;visibility:hidden;white-space:nowrap}' +
      '.emw-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;
    }

    // "☕ coffee" -> { emoji: "☕", word: "coffee" }: anything before the first
    // space that has no letters or digits counts as the emoji.
    function parse(item, extra) {
      var m = item.match(/^(\S+)\s+(.+)$/);
      if (m && !/[A-Za-z0-9À-ɏ]/.test(m[1])) return { emoji: m[1], word: m[2] };
      return { emoji: extra || '', word: item };
    }

    function setup(el) {
      var d = el.dataset;
      var emojis = (d.emojis || '').split(',').map(function (e) { return e.trim(); });
      var items = (el.getAttribute('data-emoji-word-rotator') || el.textContent)
        .split(',').map(function (w) { return w.trim(); }).filter(Boolean)
        .map(function (w, k) { return parse(w, emojis[k]); });
      el.classList.add('emw');
      el.textContent = '';
      span('emw-sr', el, items.map(function (it) { return it.word; }).join(', '));
      var after = (d.position || DEFAULTS.position) === 'after';
      var emoji = span('emw-emoji', el);
      var face = span('emw-face', emoji, items[0].emoji);
      var clip = span('emw-clip', el);
      var out = span('emw-word', clip, items[0].word);
      if (after) el.insertBefore(clip, emoji);
      emoji.style.margin = after ? '0 0 0 .12em' : '0 .12em 0 0';
      var probe = span('emw-probe', el);
      [emoji, clip, probe].forEach(function (n) { n.setAttribute('aria-hidden', 'true'); });
      return { el: el, items: items, face: face, clip: clip, 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.items[s.i].word);
      s.clip.style.width = s.w + 'px';
    }

    function swap(s, done) {
      var t = +s.el.dataset.duration || DEFAULTS.duration;
      s.i = (s.i + 1) % s.items.length;
      var next = s.items[s.i];
      s.busy = true;

      // The emoji turns edge-on, changes, and carries on round with a pop.
      anime.animate(s.face, {
        rotateY: [0, 90], scale: [1, 0.8], duration: t * 0.35, ease: 'in(2)',
        onComplete: function () {
          s.face.textContent = next.emoji;
          anime.animate(s.face, { rotateY: [-90, 0], scale: [0.8, 1], duration: t * 0.65, ease: 'outBack(2.2)' });
        }
      });

      // The old word slides up and out as the new one rises in below it.
      var old = s.out;
      old.classList.add('emw-old');
      anime.animate(old, { y: ['0em', '-1.5em'], opacity: [1, 0], duration: t * 0.6, ease: 'inOut(3)', onComplete: function () { old.remove(); } });
      s.out = span('emw-word', s.clip, next.word);
      anime.animate(s.out, { y: ['1.5em', '0em'], opacity: [0, 1], duration: t * 0.75, delay: t * 0.15, ease: 'out(4)' });
      anime.animate(s, {
        w: measure(s, next.word), duration: t * 0.45, ease: 'inOut(2)',
        onUpdate: function () { s.clip.style.width = s.w.toFixed(2) + 'px'; }
      });
      setTimeout(function () { s.busy = false; done(); }, t + 50);
    }

    // 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._emw;
          s.visible = e.isIntersecting;
          if (s.visible && !s.started) { s.started = true; start(s); }
        });
      }, { threshold: 0.3 });
      document.querySelectorAll('[data-emoji-word-rotator]').forEach(function (el) {
        if (el._emw) return;
        el._emw = setup(el);
        if (!reduced && el._emw.items.length > 1) io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<h1>
  Made with
  <span data-emoji-word-rotator="❤️ love, ☕ coffee, ✨ magic">love</span>
</h1>