โ† Particles

Emoji Rain

A shower of emoji falls from the top of the screen, each one swaying, spinning and tumbling out at the bottom. Fire it from a button, or let a section drizzle emoji while it’s in view.

It rains over the whole screen

How to use it

  1. Paste the snippet just before </head>.
  2. Add data-emoji-rain with the emoji you want, separated by spaces, to a button. Each click starts a shower over the whole screen.

Options

Add any of these attributes to the same element.

data-within="self"Rain inside the element (behind its content) instead of over the screen.
data-trigger="view"Also rain once when it scrolls into view.
data-loopKeep a steady drizzle going while it’s in view. Use with data-within="self" on a section.
data-countEmoji per shower. Default 40.
data-sizeSize of the biggest, in px. Default 40.
data-durationTime to fall. Default 2600 ms.

The emoji never block clicks and are hidden from screen readers. With reduced motion nothing falls. From code, animacious.emojiRain("๐ŸŽ‰ ๐ŸŽŠ") starts a shower, e.g. after a sign-up.

1 ยท Snippet
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
  (function () {
    // Emoji Rain: a shower of emoji falls from the top of the screen, each one
    // swaying, spinning and tumbling out at the bottom. Click the element to
    // start a shower, or let it rain inside a section.
    // Usage: <button data-emoji-rain="๐ŸŽ‰ ๐Ÿฅณ โœจ">Party</button>   (rains over the
    // whole screen on click)
    // Optional: data-within="self" (rain inside the element instead of the
    // screen), data-trigger="view" (rain once when it scrolls into view,
    // as well as on click), data-loop (keep raining while in view), data-count
    // (emoji per shower, default 40), data-size (largest, in px, default 40),
    // data-duration (ms it takes to fall, default 2600).
    // Script use: animacious.emojiRain("๐ŸŽ‰ ๐ŸŽŠ") rains over the screen;
    // animacious.emojiRain("๐ŸŽ‰", element) rains inside an element.
    var DEFAULTS = { emoji: '๐ŸŽ‰ ๐Ÿฅณ โœจ ๐ŸŽŠ ๐Ÿ’œ', count: 40, size: 40, duration: 2600 };

    var css =
      '.emr-layer{position:fixed;inset:0;overflow:hidden;pointer-events:none;z-index:9999}' +
      // Inside an element, it rains behind the element's content.
      '.emr-in{position:absolute;z-index:-1}' +
      '.emr-host{isolation:isolate}' +
      '.emr-drop{position:absolute;top:0;left:0;line-height:1;user-select:none;will-change:transform}';
    var style = document.createElement('style');
    style.textContent = css;
    document.head.appendChild(style);

    var screenLayer = null;

    function layerFor(host) {
      if (!host) {
        if (!screenLayer || !screenLayer.isConnected) {
          screenLayer = document.createElement('div');
          screenLayer.className = 'emr-layer';
          screenLayer.setAttribute('aria-hidden', 'true');
          document.body.appendChild(screenLayer);
        }
        return screenLayer;
      }
      if (!host._emrLayer) {
        if (getComputedStyle(host).position === 'static') host.style.position = 'relative';
        host.classList.add('emr-host');
        var l = document.createElement('div');
        l.className = 'emr-layer emr-in';
        l.setAttribute('aria-hidden', 'true');
        host.appendChild(l);
        host._emrLayer = l;
      }
      return host._emrLayer;
    }

    function parse(list) {
      // Split on spaces or commas, keeping multi-part emoji whole.
      var items = String(list || DEFAULTS.emoji).split(/[\s,]+/).filter(Boolean);
      return items.length ? items : DEFAULTS.emoji.split(' ');
    }

    // One emoji falling from above the top to below the bottom.
    function drop(layer, emoji, opts, wait) {
      var R = anime.utils.random;
      var W = layer.clientWidth, H = layer.clientHeight;
      var s = opts.size * R(0.45, 1, 2);
      var e = document.createElement('span');
      e.className = 'emr-drop';
      e.textContent = emoji;
      e.style.fontSize = s + 'px';
      layer.appendChild(e);
      var x = R(-s * 0.3, W - s * 0.7);
      var dur = opts.duration * R(0.75, 1.25, 2) * Math.max(0.6, Math.min(1.4, H / 700));
      var sway = R(14, 46) * (Math.random() < 0.5 ? -1 : 1);
      anime.utils.set(e, { x: x, y: -s * 1.4, rotate: R(-30, 30) });
      anime.animate(e, {
        y: { to: H + s * 0.4, ease: 'in(1.3)' },
        x: [{ to: x + sway, ease: 'inOut(2)' }, { to: x - sway * 0.4, ease: 'inOut(2)' }, { to: x + sway * 0.6, ease: 'inOut(2)' }],
        rotate: { to: R(-200, 200), ease: 'linear' },
        // A little wobble in scale, like it's tumbling.
        scale: [{ to: 0.85, ease: 'inOut(2)' }, { to: 1.08, ease: 'inOut(2)' }, { to: 1, ease: 'inOut(2)' }],
        duration: dur,
        delay: wait,
        onComplete: function () {
          e.remove();
          if (layer === screenLayer && !layer.children.length) { layer.remove(); screenLayer = null; }
        }
      });
    }

    function rain(emoji, host, opts) {
      if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
      opts = opts || {};
      var o = {
        count: opts.count || DEFAULTS.count,
        size: opts.size || DEFAULTS.size,
        duration: opts.duration || DEFAULTS.duration,
        spread: opts.spread !== undefined ? opts.spread : 1400
      };
      var layer = layerFor(host);
      var list = parse(emoji);
      for (var i = 0; i < o.count; i++) {
        drop(layer, list[i % list.length], o, Math.pow(Math.random(), 1.3) * o.spread);
      }
    }

    function optsOf(el) {
      var d = el.dataset;
      return { count: +d.count || 0, size: +d.size || 0, duration: +d.duration || 0 };
    }

    function init() {
      var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
      document.querySelectorAll('[data-emoji-rain]').forEach(function (el) {
        if (el._emr) return;
        el._emr = true;
        var d = el.dataset;
        var host = d.within === 'self' ? el : null;
        var loop = d.loop !== undefined && d.loop !== 'false';
        el.addEventListener('click', function () { rain(d.emojiRain, host, optsOf(el)); });
        if ((d.trigger !== 'view' && !loop) || reduced) return;
        var timer = null;
        var io = new IntersectionObserver(function (entries) {
          clearInterval(timer);
          if (!entries[0].isIntersecting) return;
          if (!loop) { io.disconnect(); rain(d.emojiRain, host, optsOf(el)); return; }
          // A steady drizzle: a few at a time, spread out.
          var o = optsOf(el);
          var every = 400;
          var per = Math.max(1, Math.round(((o.count || DEFAULTS.count) * every) / (o.duration || DEFAULTS.duration) / 2));
          var tick = function () {
            if (!el.closest('[hidden]')) rain(d.emojiRain, host, { count: per, size: o.size, duration: o.duration, spread: every });
          };
          tick();
          timer = setInterval(tick, every);
        }, { threshold: 0.3 });
        io.observe(el);
      });
    }

    window.animacious = window.animacious || {};
    window.animacious.emojiRain = function (emoji, el, opts) { rain(emoji, el || null, opts); };
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
    else init();
  })();
</script>
2 ยท Your HTML
<button data-emoji-rain="๐ŸŽ‰ ๐Ÿฅณ โœจ">Celebrate</button>

<!-- a section that drizzles hearts while it's in view: -->
<section data-emoji-rain="๐Ÿ’œ ๐Ÿ’–" data-within="self" data-loop>โ€ฆ</section>