Stagger Drop

transform

Parts drop in from above one after another and bounce as they land. Position and fade are separate animations, since they want different eases.

Demo shape

Every animation on the page redraws with the shape you pick.

Align
Ease
Duration
s
0.13
Stagger
s
00.2
Delay
s
02
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-stagger-drop" aria-hidden="true">
  <circle cx="60" cy="60" r="34" fill="none" stroke="currentColor" stroke-width="8" vector-effect="non-scaling-stroke" />
  <line x1="60" y1="12" x2="60" y2="38" stroke="currentColor" stroke-width="8" stroke-linecap="round" vector-effect="non-scaling-stroke" />
  <line x1="60" y1="82" x2="60" y2="108" stroke="currentColor" stroke-width="8" stroke-linecap="round" vector-effect="non-scaling-stroke" />
  <circle cx="60" cy="60" r="10" fill="currentColor" />
</svg>

// 2. JS
function staggerDrop(svg) {
  const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
  const _duration = _reducedMotion ? 0.01 : 0.9
  const _stagger = _reducedMotion ? 0 : 0.09
  const _delay = _reducedMotion ? 0 : 0

  const parts = paths(svg)
  const easing = 'linear(0.0000,0.0022,0.0087,0.0196,0.0348,0.0543,0.0782,0.1065,0.1390,0.1760,0.2173,0.2629,0.3128,0.3672,0.4258,0.4888,0.5562,0.6279,0.7039,0.7843,0.8690,0.9581,0.9752,0.9332,0.8954,0.8621,0.8330,0.8083,0.7880,0.7720,0.7603,0.7530,0.7501,0.7515,0.7572,0.7673,0.7817,0.8004,0.8235,0.8510,0.8828,0.9189,0.9594,0.9979,0.9772,0.9608,0.9487,0.9410,0.9377,0.9386,0.9440,0.9537,0.9677,0.9860,0.9960,0.9881,0.9846,0.9854,0.9905,1.0000)'
  const ms = _duration * 1000
  // Position and opacity are separate animations because they want different
  // eases — a bounce on the fade would flicker. They touch different
  // properties, so neither overwrites the other.
  const animations = parts.flatMap((part, i) => {
    const delay = _delay * 1000 + i * _stagger * 1000
    return [
      part.animate([{ transform: 'translateY(-70px)' }, { transform: 'translateY(0px)' }], { duration: ms, delay, easing, fill: 'both' }),
      part.animate([{ opacity: 0 }, { opacity: 1 }], {
        duration: ms * 0.25,
        delay,
        easing: 'linear(0.0000,0.0506,0.0999,0.1479,0.1946,0.2400,0.2840,0.3268,0.3682,0.4083,0.4471,0.4845,0.5207,0.5556,0.5891,0.6213,0.6522,0.6818,0.7101,0.7370,0.7627,0.7870,0.8100,0.8317,0.8521,0.8711,0.8889,0.9053,0.9204,0.9343,0.9467,0.9579,0.9678,0.9763,0.9836,0.9895,0.9941,0.9974,0.9993,1.0000)',
        fill: 'both',
      }),
    ]
  })

  return () => animations.forEach((a) => a.cancel())
}

function paths(svg) {
  return [...svg.querySelectorAll('path, line, polyline, polygon, circle, ellipse, rect')].filter(
    (el) => !el.closest('defs, clipPath, mask'),
  )
}


staggerDrop(document.querySelector('.k-stagger-drop'))