CSS + JS Scenes & objects #generated #pointer

Confetti burst in CSS + JavaScript

Click anywhere. JS spawns particles with random 3D vectors; one CSS animation flies them all.

  • 33 lines of CSS
  • 1 lines of HTML
  • 17 lines of JS
  • No dependencies
  • MIT licensed
GitHub

How it works

  1. On click, JS creates 36 particles at the pointer position. Each gets a random vector in custom properties: --x, --y, --z, --spin, --hue.
  2. There is only one keyframe rule. It reads those variables, so every particle flies somewhere different.
  3. --z is what makes it 3D: particles coming toward the camera grow, the others shrink away.
  4. Each particle removes itself on animationend, so the DOM never fills up.

Key techniques

  • random --x / --y / --z / --spin per particle
  • translate3d + rotate3d keyframes
  • remove on animationend

Copy-paste code

HTML 1 lines

<div class="party">click anywhere</div>

CSS 33 lines

.party {
  position: fixed;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 500px;
  color: #949bc0;
  font-family: system-ui;
  cursor: pointer;
  user-select: none;
}

.party i {
  position: absolute;
  width: 10px;
  height: 14px;
  margin: -7px 0 0 -5px;
  border-radius: 2px;
  background: hsl(var(--hue) 90% 62%);
  pointer-events: none;
  animation: fly 1.3s cubic-bezier(0.1, 0.7, 0.3, 1) forwards;
}

@keyframes fly {
  to {
    opacity: 0;
    /* + 160px on Y is the "gravity" */
    transform:
      translate3d(var(--x), calc(var(--y) + 160px), var(--z))
      rotate3d(1, 1, 0.4, var(--spin));
  }
}

JS 17 lines

const party = document.querySelector('.party');
const rand = (min, max) => min + Math.random() * (max - min);

party.addEventListener('pointerdown', (e) => {
  for (let i = 0; i < 36; i++) {
    const p = document.createElement('i');
    p.style.left = e.clientX + 'px';
    p.style.top = e.clientY + 'px';
    p.style.setProperty('--x', rand(-220, 220) + 'px');
    p.style.setProperty('--y', rand(-240, 80) + 'px');
    p.style.setProperty('--z', rand(-200, 300) + 'px');
    p.style.setProperty('--spin', rand(360, 1080) + 'deg');
    p.style.setProperty('--hue', rand(0, 360));
    p.addEventListener('animationend', () => p.remove(), { once: true });
    party.append(p);
  }
});

Sass source 35 lines

.d-confetti {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  overflow: hidden;
  perspective: 500px;
  cursor: pointer;
  user-select: none;

  span {
    color: var(--muted);
    font-size: 13px;
  }

  // Every particle gets --x --y --z --spin --hue from JS and runs the same animation.
  i {
    position: absolute;
    width: 8px;
    height: 12px;
    margin: -6px 0 0 -4px;
    border-radius: 2px;
    background: hsl(var(--hue) 90% 62%);
    pointer-events: none;
    animation: d-confetti-fly 1.3s cubic-bezier(0.1, 0.7, 0.3, 1) forwards;
  }
}

@keyframes d-confetti-fly {
  to {
    opacity: 0;
    // + 120px on Y is the "gravity"
    transform: translate3d(var(--x), calc(var(--y) + 120px), var(--z)) rotate3d(1, 1, 0.4, var(--spin));
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.