← SVG & icons

Animated Emoji

Drawn emoji faces with little loops of their own. One smiles, blinks and glances about, one rocks with laughter till tears of joy fly off, one winks with a sparkle and one sobs as tears roll down its cheeks.

Blink
Laugh
Wink
Cry

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-animated-emoji to an empty <span>, set to the face you want: blink, laugh, wink or cry.
  3. Size it with font-size: the face is 1em square, so it sits nicely in a line of text.

Options

Add any of these attributes to the same element.

data-colorFace colour. Default #fcd34d.

Each face gets an aria-label such as “Winking face”. The loops pause while off screen, and stay still with reduced motion.

1 · Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Animated Emoji: drawn emoji faces with little loops of their own. One
    // blinks and glances about, one laughs till it cries, one winks with a
    // sparkle and one sobs, tears rolling down its cheeks.
    // Usage: <span data-animated-emoji="laugh"></span>
    //   Faces: blink, laugh, wink, cry. Size it with font-size (1em square).
    // Optional: data-color (face colour, default #fcd34d).
    // The loops pause while off screen, and stay still with reduced motion.
    var LABELS = { blink: 'Smiling face', laugh: 'Laughing face', wink: 'Winking face', cry: 'Crying face' };
    var DEFAULTS = { color: '#fcd34d' };
    var NS = 'http://www.w3.org/2000/svg';
    var INK = '#5b3a12';
    var count = 0;

    var css =
      '.aem{display:inline-block;width:1em;height:1em;line-height:0;vertical-align:middle}' +
      '.aem svg{display:block;width:100%;height:100%;overflow:visible}' +
      '.aem svg [class]{transform-box:fill-box;transform-origin:center}' +
      '.aem-off *{animation-play-state:paused!important}' +
      // blink
      '.aem-blink{animation:aem-blink 4s linear infinite}' +
      '.aem-look{animation:aem-look 4s ease-in-out infinite}' +
      // laugh
      '.aem-shake{transform-origin:50% 80%!important;animation:aem-shake .5s ease-in-out infinite}' +
      '.aem-jaw{transform-origin:50% 0!important;animation:aem-jaw .25s ease-in-out infinite alternate}' +
      '.aem-joy{animation:aem-joy 1.4s ease-out infinite}' +
      '.aem-joy+.aem-joy{animation-name:aem-joy2;animation-delay:-.7s}' +
      // wink
      '.aem-tilt{transform-origin:50% 50%!important;animation:aem-tilt 3.2s ease-in-out infinite}' +
      '.aem-open{animation:aem-open 3.2s linear infinite}' +
      '.aem-shut{animation:aem-shut 3.2s linear infinite}' +
      '.aem-spark{animation:aem-spark 3.2s ease-out infinite}' +
      // cry
      '.aem-sob{animation:aem-sob 2.6s ease-in-out infinite}' +
      '.aem-tear{animation:aem-tear 1.3s ease-in infinite}' +
      '.aem-quiver{transform-origin:50% 0!important;animation:aem-quiver .35s ease-in-out infinite alternate}' +
      '@keyframes aem-blink{0%,44%,49%,100%{transform:scaleY(1)}46.5%{transform:scaleY(.1)}}' +
      '@keyframes aem-look{0%,15%,85%,100%{transform:translate(0,0)}25%,40%{transform:translate(-2.5px,-.5px)}55%,72%{transform:translate(2.5px,-.5px)}}' +
      '@keyframes aem-shake{0%,100%{transform:translateY(0) rotate(-3deg)}50%{transform:translateY(-1.5px) rotate(3deg)}}' +
      '@keyframes aem-jaw{from{transform:scaleY(.82)}to{transform:scaleY(1.05)}}' +
      '@keyframes aem-joy{0%{transform:translate(0,0) scale(.4);opacity:0}15%{opacity:1}100%{transform:translate(-7px,8px) scale(1);opacity:0}}' +
      '@keyframes aem-joy2{0%{transform:translate(0,0) scale(.4);opacity:0}15%{opacity:1}100%{transform:translate(7px,8px) scale(1);opacity:0}}' +
      '@keyframes aem-tilt{0%,30%,85%,100%{transform:rotate(0)}42%,72%{transform:rotate(-8deg)}}' +
      '@keyframes aem-open{0%,36%,78%,100%{transform:scaleY(1);opacity:1}40%,74%{transform:scaleY(.1);opacity:0}}' +
      '@keyframes aem-shut{0%,36%,78%,100%{opacity:0}40%,74%{opacity:1}}' +
      '@keyframes aem-spark{0%,38%{transform:scale(0) rotate(0);opacity:0}46%{transform:scale(1.2) rotate(45deg);opacity:1}62%{transform:scale(.9) rotate(90deg);opacity:1}72%,100%{transform:scale(0) rotate(120deg);opacity:0}}' +
      '@keyframes aem-sob{0%,100%{transform:translateY(0)}10%,30%{transform:translateY(1px)}20%,40%{transform:translateY(-.5px)}}' +
      '@keyframes aem-tear{0%{transform:translateY(-2px) scale(.5);opacity:0}15%{opacity:1;transform:translateY(0) scale(1)}100%{transform:translateY(17px) scale(1);opacity:0}}' +
      '@keyframes aem-quiver{from{transform:scaleY(1) translateY(0)}to{transform:scaleY(.86) translateY(.6px)}}' +
      '@media (prefers-reduced-motion:reduce){.aem svg *{animation:none!important}.aem-shut,.aem-spark,.aem-joy{opacity:0}}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    function eye(x, cls) {
      return '<ellipse class="' + cls + '" cx="' + x + '" cy="26" rx="3.4" ry="4.8" fill="' + INK + '"/>';
    }
    function drop(x, y, cls, extra) {
      return '<path class="' + cls + '"' + (extra || '') + ' d="M' + x + ' ' + y + ' C' + (x - 3) + ' ' + (y + 4) + ' ' + (x - 3) + ' ' + (y + 7) + ' ' + x + ' ' + (y + 7) +
        ' C' + (x + 3) + ' ' + (y + 7) + ' ' + (x + 3) + ' ' + (y + 4) + ' ' + x + ' ' + y + ' Z" fill="#60a5fa" stroke="#3b82f6" stroke-width=".8"/>';
    }
    var CHEEKS = '<ellipse cx="16" cy="38" rx="4.5" ry="2.8" fill="#fb7185" opacity=".35"/><ellipse cx="48" cy="38" rx="4.5" ry="2.8" fill="#fb7185" opacity=".35"/>';

    var FACES = {
      blink: function () {
        return '<g class="aem-look"><g class="aem-blink">' + eye(23, '') + eye(41, '') + '</g></g>' + CHEEKS +
          '<path d="M21 39 Q32 50 43 39" fill="none" stroke="' + INK + '" stroke-width="3.2" stroke-linecap="round"/>';
      },
      laugh: function () {
        return '<g fill="none" stroke="' + INK + '" stroke-width="3.2" stroke-linecap="round" stroke-linejoin="round">' +
          '<path d="M17 28 L24 24.5 L17 21"/><path d="M47 28 L40 24.5 L47 21"/></g>' + CHEEKS +
          '<g class="aem-jaw"><path d="M18 35 H46 Q45 52 32 52 Q19 52 18 35 Z" fill="' + INK + '"/>' +
          '<path d="M20 36 H44 Q44 39 42.5 40 H21.5 Q20 39 20 36 Z" fill="#fff"/>' +
          '<path d="M24 48.5 Q32 43 40 48.5 Q36.5 51 32 51 Q27.5 51 24 48.5 Z" fill="#fb7185"/></g>' +
          drop(14, 27, 'aem-joy') + drop(50, 27, 'aem-joy');
      },
      wink: function () {
        return '<g class="aem-tilt">' + eye(23, '') + eye(41, 'aem-open') +
          '<path class="aem-shut" d="M36.5 27 Q41 22.5 45.5 27" fill="none" stroke="' + INK + '" stroke-width="3.2" stroke-linecap="round"/>' + CHEEKS +
          '<path d="M22 41 Q33 49 43 38" fill="none" stroke="' + INK + '" stroke-width="3.2" stroke-linecap="round"/>' +
          '<path d="M37.5 43 Q38.5 48 42 47 Q44 45.5 42.5 41" fill="#fb7185" stroke="' + INK + '" stroke-width="1.6" stroke-linejoin="round"/></g>' +
          '<path class="aem-spark" d="M56 10 L57.6 14.4 L62 16 L57.6 17.6 L56 22 L54.4 17.6 L50 16 L54.4 14.4 Z" fill="#fde047" stroke="#eab308" stroke-width=".8"/>';
      },
      cry: function () {
        return '<g class="aem-sob"><g fill="none" stroke="' + INK + '" stroke-width="2.8" stroke-linecap="round">' +
          '<path d="M16 21 L26 17"/><path d="M38 17 L48 21"/>' +
          '<path d="M18 28 Q23 31.5 28 28"/><path d="M36 28 Q41 31.5 46 28"/></g>' +
          '<rect x="20.5" y="30" width="5" height="16" rx="2.5" fill="#93c5fd" opacity=".55"/><rect x="38.5" y="30" width="5" height="16" rx="2.5" fill="#93c5fd" opacity=".55"/>' +
          '<g class="aem-quiver"><path d="M24 47 Q32 39 40 47" fill="none" stroke="' + INK + '" stroke-width="3.2" stroke-linecap="round"/></g></g>' +
          drop(23, 31, 'aem-tear') + drop(41, 31, 'aem-tear', ' style="animation-delay:-.6s"');
      }
    };

    function setup(el) {
      var name = el.getAttribute('data-animated-emoji') || 'blink';
      if (!FACES[name]) name = 'blink';
      var id = 'aem-grad-' + ++count;
      var color = el.dataset.color || DEFAULTS.color;
      el.classList.add('aem', 'aem-off');
      var svg = document.createElementNS(NS, 'svg');
      svg.setAttribute('viewBox', '0 0 64 64');
      svg.setAttribute('aria-hidden', 'true');
      svg.innerHTML =
        '<defs><radialGradient id="' + id + '" cx=".38" cy=".32" r=".75"><stop offset="0" stop-color="#fff" stop-opacity=".45"/><stop offset=".5" stop-color="#fff" stop-opacity="0"/><stop offset="1" stop-color="#b45309" stop-opacity=".22"/></radialGradient></defs>' +
        '<g' + (name === 'laugh' ? ' class="aem-shake"' : '') + '>' +
        '<circle cx="32" cy="32" r="28" fill="' + color + '"/><circle cx="32" cy="32" r="28" fill="url(#' + id + ')" stroke="#d97706" stroke-width="1.4"/>' +
        FACES[name]() + '</g>';
      el.appendChild(svg);
      if (!el.getAttribute('aria-label') && !el.closest('[aria-hidden="true"]')) {
        el.setAttribute('role', 'img');
        el.setAttribute('aria-label', LABELS[name]);
      }
    }

    function init() {
      var io = new IntersectionObserver(function (entries) {
        entries.forEach(function (e) { e.target.classList.toggle('aem-off', !e.isIntersecting); });
      });
      document.querySelectorAll('[data-animated-emoji]').forEach(function (el) {
        if (el._aem) return;
        el._aem = true;
        setup(el);
        io.observe(el);
      });
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 · Your HTML
<p>That was brilliant <span data-animated-emoji="laugh"></span></p>

<span data-animated-emoji="wink" style="font-size: 96px"></span>