Emoji Rain Effect
Clicking a trigger button launches a shower of emoji images that rise up from the bottom of the viewport. Two emoji types alternate per button, with random sizes, delays, speeds, and a swaying child animation.
GSAPInteractiveFunAnimation
Setup — External Scripts
GSAP CDN
html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.12.5/dist/gsap.min.js"></script>Code
index.html
html
<!-- Place this container just after the opening <body> tag, outside <main> -->
<div data-emoji-rain-container class="emoji-rain-container">
<div class="single-rain-emoji hidden">
<div class="single-rain-emoji-image-fire"></div>
<div class="single-rain-emoji-image-love"></div>
<div class="single-rain-emoji-image-shame"></div>
<div class="single-rain-emoji-image-thumbs-down"></div>
</div>
</div>
<!-- Trigger buttons -->
<div class="btn-wrap">
<div data-hover data-emoji-rain-type-1="fire" data-emoji-rain-type-2="love" class="emoji-rain-btn"><span>Emoji Rain Fire & Love</span></div>
<div data-hover data-emoji-rain-type-1="thumbs-down" data-emoji-rain-type-2="shame" class="emoji-rain-btn"><span>Thumbs Down & Shame</span></div>
</div>styles.css
css
.emoji-rain-container {
z-index: 150;
pointer-events: none;
-webkit-user-select: none;
user-select: none;
position: fixed;
inset: 0%;
overflow: hidden;
}
.single-rain-emoji {
will-change: transform;
width: max(200px, 15vw);
position: absolute;
}
.single-rain-emoji.hidden {
opacity: 0;
}
.single-rain-emoji-image-fire {
background-image: url('../images/icon-3d-fire.png');
background-position: 50%;
background-size: cover;
width: 100%;
padding-top: 100%;
}
.single-rain-emoji-image-love {
background-image: url('../images/icon-3d-love.png');
background-position: 50%;
background-size: cover;
width: 100%;
padding-top: 100%;
}
.single-rain-emoji-image-shame {
background-image: url('../images/icon-3d-shame.png');
background-position: 50%;
background-size: cover;
width: 100%;
padding-top: 100%;
}
.single-rain-emoji-image-thumbs-down {
background-image: url('../images/icon-3d-thumbsup.png');
background-position: 50%;
background-size: cover;
width: 100%;
padding-top: 100%;
rotate: 180deg;
}
.btn-wrap {
grid-column-gap: 1.5em;
grid-row-gap: 1.5em;
flex-flow: column;
align-items: center;
display: flex;
}
.emoji-rain-btn {
grid-column-gap: .125em;
grid-row-gap: .125em;
cursor: pointer;
background-color: #fff;
border-radius: 10em;
align-items: center;
padding: .5em .75em .5em 1em;
font-family: PP Neue Corp Normal, Arial, sans-serif;
font-size: 2.5em;
font-weight: 700;
transition-property: all;
transition-duration: .3s;
transition-timing-function: cubic-bezier(.45, .422, .269, 1.702);
display: flex;
transform: scale(1)rotate(.001deg);
}
.emoji-rain-btn:hover {
transform: scale(1.05)rotate(.001deg);
}script.js
javascript
let emojiAnimationRunning = false;
function initEmojiRain(emojiTypes, emojiContainer) {
if (emojiAnimationRunning) return;
emojiAnimationRunning = true;
const emojiContainerHeight = emojiContainer.offsetHeight;
const emojiQuantity = 60;
const getRandomInt = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min;
const createEmojiElement = () => {
const emojiScale = Math.random() * 0.6 + 0.4;
const emojiRotate = getRandomInt(1, 5);
const emojiDelay = 0.001 * getRandomInt(0, 1250);
const emojiSpeed = getRandomInt(500, 1500) * 0.001;
const emojiPosition = `${getRandomInt(0, 10)}0%`;
const emojiClass = `single-rain-emoji-image-${emojiTypes[Math.floor(Math.random() * emojiTypes.length)]}`;
const singleEmoji = document.createElement("div");
singleEmoji.className = "single-rain-emoji append";
singleEmoji.style.left = emojiPosition;
const singleEmojiChild = document.createElement("div");
singleEmojiChild.className = emojiClass;
singleEmoji.appendChild(singleEmojiChild);
gsap.fromTo(
singleEmoji,
{ y: emojiContainerHeight, xPercent: -50, rotate: 0.001, scale: emojiScale },
{ y: "-100%", xPercent: -50, rotate: 0.001, delay: emojiDelay, ease: "Power1.easeIn", duration: emojiSpeed }
);
gsap.fromTo(
singleEmojiChild,
{ xPercent: -25, rotate: emojiRotate },
{ xPercent: 25, rotate: -emojiRotate, ease: "Power1.easeInOut", delay: emojiDelay, duration: 0.8, repeat: -1, yoyo: true }
);
emojiContainer.appendChild(singleEmoji);
};
Array.from({ length: emojiQuantity }).forEach(createEmojiElement);
setTimeout(() => {
emojiContainer.querySelectorAll(".single-rain-emoji.append").forEach(el => el.remove());
emojiAnimationRunning = false;
}, 2750);
}
function initEmojiRainActions() {
document.querySelectorAll("[data-emoji-rain-type-1]").forEach(trigger => {
trigger.addEventListener("click", () => {
const type1 = trigger.getAttribute("data-emoji-rain-type-1");
const type2 = trigger.getAttribute("data-emoji-rain-type-2") || type1;
const emojiContainer = document.querySelector("[data-emoji-rain-container]");
if (!emojiContainer) return;
initEmojiRain([type1, type2], emojiContainer);
});
});
}
document.addEventListener('DOMContentLoaded', () => {
initEmojiRainActions();
});Notes
- •A global `emojiAnimationRunning` flag prevents a second rain from stacking on top while one is already in progress.
- •Emoji positions are snapped to 10% increments (`getRandomInt(0,10) * 10%`) giving natural spread without the emojis hugging edges.
- •Each emoji has two simultaneous GSAP tweens — one driving it upward on the parent div, and one creating a horizontal sway on the child image div.
- •All appended elements are cleaned up via `querySelectorAll('.append')` after 2750 ms, matching the maximum possible delay + duration.
Guide
Placement
Put the `.emoji-rain-container` just after the opening `<body>` tag, outside `<main>`, so it sits above all other content at `z-index: 150`.
Adding new emoji types
Add a new child div inside `.single-rain-emoji.hidden` with a class following the pattern `single-rain-emoji-image-{type}`. Then reference that type name in `data-emoji-rain-type-1` or `data-emoji-rain-type-2` on any trigger button.