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 &amp; 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 &amp; 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.