← Text effects

Emoji Swap

Every so often a random letter pops into an emoji, holds for a beat, then pops back. Where it can it picks one shaped like the letter, so an O might turn into a doughnut or a football. Hover over or tap a letter to swap it yourself.

Having fun today

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-emoji-swap to any heading or text element.

Options

Add any of these attributes to the same element.

data-emojiYour own emoji to pick from, separated by commas, e.g. data-emoji="πŸŽ‰,✨,πŸ”₯". Turns off the look-alike matching.
data-intervalTime between swaps. Default 650 ms.
data-holdHow long each emoji stays. Default 1100 ms.
data-maxHow many letters can be emoji at once. Default 2.
1 Β· Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Emoji Swap: every so often a random letter pops into an emoji, holds for
    // a moment, then pops back. Where it can, it picks an emoji shaped like
    // the letter (an O becomes a doughnut or a ball). Tap or hover over a
    // letter to swap it yourself.
    // Usage: <h1 data-emoji-swap>Having fun</h1>
    // Optional: data-emoji (your own emoji to pick from, separated by commas;
    // turns off the look-alike matching), data-interval (ms between swaps;
    // default 650), data-hold (ms an emoji stays; default 1100), data-max (how
    // many letters can be emoji at once; default 2).
    var DEFAULTS = { interval: 650, hold: 1100, max: 2 };

    // Emoji that look a bit like the letter, then a general mix.
    var LOOKALIKE = {
      o: ['🍩', '⚽', 'πŸŒ•', 'πŸͺ', 'β­•', 'πŸ€'],
      a: ['🍎', 'πŸ…°οΈ'], b: ['πŸ…±οΈ', '🐝'],
      c: ['πŸŒ™', 'πŸ₯'], d: ['πŸ—'], e: ['πŸ“§', 'πŸ₯š'],
      g: ['πŸŒ€'], h: ['πŸͺœ'], i: ['πŸ•―οΈ', 'ℹ️', '🍑'],
      j: ['πŸŒ‚'], l: ['πŸ“', 'πŸ•―οΈ'], m: ['Ⓜ️', '〰️'],
      n: ['πŸŒ‰'], p: ['πŸ…ΏοΈ', '🍭'], q: ['🍳'],
      s: ['πŸ’²', '🐍'], t: ['🌴', '✝️'], u: ['🧲'],
      v: ['βœ…', 'πŸ”½'], w: ['〰️'], x: ['❌', 'βš”οΈ'],
      y: ['🍸', 'πŸͺƒ'], z: ['⚑', 'πŸ’€'], 0: ['🍩', '⚽'],
      1: ['πŸ•―οΈ'], 8: ['♾️', 'β››'], '!': ['❗'], '?': ['❓']
    };
    var MIX = ['πŸ˜€', '😎', '🀩', 'πŸŽ‰', '✨', 'πŸ”₯', 'πŸ’œ', '⭐', '🌈', 'πŸš€', 'πŸ•', 'πŸ¦„'];

    var css =
      '.ejs-word{display:inline-block;white-space:nowrap}' +
      '.ejs-ch{display:inline-block;position:relative}' +
      '.ejs-glyph{display:inline-block}' +
      '.ejs-emo{position:absolute;left:50%;top:50%;font-size:.78em;line-height:1;letter-spacing:0;transform:translate(-50%,-50%) scale(0);font-family:"Apple Color Emoji","Segoe UI Emoji","Noto Color Emoji",sans-serif;pointer-events:none}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

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

    function split(el) {
      var text = el.textContent.replace(/\s+/g, ' ').trim();
      el.setAttribute('aria-label', text);
      el.textContent = '';
      var chars = [];
      text.split(' ').forEach(function (w, i) {
        if (i) el.appendChild(document.createTextNode(' '));
        var word = document.createElement('span');
        word.className = 'ejs-word';
        word.setAttribute('aria-hidden', 'true');
        Array.from(w).forEach(function (c) {
          var s = document.createElement('span');
          s.className = 'ejs-ch';
          var g = document.createElement('span');
          g.className = 'ejs-glyph';
          g.textContent = c;
          var e = document.createElement('span');
          e.className = 'ejs-emo';
          s.appendChild(g);
          s.appendChild(e);
          word.appendChild(s);
          chars.push({ el: s, glyph: g, emo: e, ch: c, busy: false });
        });
        el.appendChild(word);
      });
      return chars;
    }

    function pick(list) { return list[Math.floor(Math.random() * list.length)]; }

    function setup(el) {
      var d = el.dataset;
      var interval = +d.interval || DEFAULTS.interval;
      var hold = +d.hold || DEFAULTS.hold;
      var max = +d.max || DEFAULTS.max;
      var custom = d.emoji ? d.emoji.split(',').map(function (s) { return s.trim(); }).filter(Boolean) : null;
      var chars = split(el);
      if (reduced() || !chars.length) return;

      chars.forEach(function (c) { anime.utils.set(c.emo, { x: '-50%', y: '-50%', scale: 0 }); });
      var active = 0;
      function emojiFor(c) {
        if (custom) return pick(custom);
        var own = LOOKALIKE[c.ch.toLowerCase()];
        return own && Math.random() < 0.75 ? pick(own) : pick(MIX);
      }

      function swap(c) {
        if (c.busy) return;
        c.busy = true; active++;
        c.emo.textContent = emojiFor(c);
        var tilt = anime.utils.random(-18, 18);
        anime.animate(c.glyph, { scale: [1, 0], rotate: [0, -tilt], duration: 180, ease: 'in(2)' });
        anime.animate(c.emo, {
          scale: [{ from: 0, to: 1.15, duration: 300, delay: 120, ease: 'outBack(2.5)' }, { to: 1, duration: 140 }, { to: 0, delay: hold, duration: 180, ease: 'in(2)' }],
          rotate: [{ from: tilt, to: 0, duration: 420, delay: 120, ease: 'outBack(2)' }, { to: -tilt, delay: hold, duration: 180 }]
        });
        setTimeout(function () {
          anime.animate(c.glyph, {
            scale: [{ from: 0, to: 1, duration: 420, ease: 'outBack(2.5)' }], rotate: [{ from: tilt, to: 0, duration: 420, ease: 'outBack(2)' }],
            onComplete: function () { c.busy = false; active--; }
          });
        }, 120 + 440 + hold + 120);
      }

      // Tap or hover a letter to swap it.
      chars.forEach(function (c) {
        c.el.addEventListener('pointerenter', function (e) { if (e.pointerType === 'mouse') { lastReal = performance.now(); swap(c); } });
        c.el.addEventListener('pointerdown', function () { lastReal = performance.now(); swap(c); });
      });

      var lastReal = -1e9, timer = null;
      var letters = chars.filter(function (c) { return /\S/.test(c.ch); });
      function tick() {
        if (active < max && performance.now() - lastReal > 1500) {
          var free = letters.filter(function (c) { return !c.busy; });
          if (free.length) swap(pick(free));
        }
        timer = setTimeout(tick, interval * anime.utils.random(0.7, 1.3, 2));
      }
      new IntersectionObserver(function (entries) {
        clearTimeout(timer);
        if (entries[0].isIntersecting) timer = setTimeout(tick, 400);
      }).observe(el);
    }

    function init() {
      document.querySelectorAll('[data-emoji-swap]').forEach(function (el) {
        if (el._ejs) return;
        el._ejs = true;
        setup(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 Β· Your HTML
<h1 data-emoji-swap>Having fun today</h1>

<!-- party emoji only, more at once: -->
<h1 data-emoji-swap data-emoji="πŸŽ‰,✨,πŸ₯³,🎈" data-max="4">Party time</h1>