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
- Paste the snippet just before
</head>. - Add
data-emoji-rainwith 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-loop | Keep a steady drizzle going while it’s in view. Use with data-within="self" on a section. |
data-count | Emoji per shower. Default 40. |
data-size | Size of the biggest, in px. Default 40. |
data-duration | Time 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.
<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><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>