Parts drop in from above one after another and bounce as they land. Position and fade are separate animations, since they want different eases.
Every animation on the page redraws with the shape you pick.
<!-- 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'))