Copies of the first part swell outward and fade, half a beat apart, forever. The copies sit behind the original, which never moves.
Every animation on the page redraws with the shape you pick.
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-pulse-ring" 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 pulseRing(svg) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return () => {}
const [ring] = paths(svg)
if (!ring) return () => {}
// Two copies behind the original, which never moves.
const copies = [0, 1].map(() => {
const copy = ring.cloneNode(true)
ring.before(copy)
return copy
})
const ms = 1.6 * 1000
const animations = copies.map((copy, i) => {
// Without fill-box each copy would swell out from the SVG's top-left corner.
copy.style.transformBox = 'fill-box'
copy.style.transformOrigin = 'center'
return copy.animate([{ transform: 'scale(1)', opacity: 0.6 }, { transform: 'scale(1.8)', opacity: 0 }], {
duration: ms,
delay: (ms / 2) * i,
iterations: Infinity,
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)',
})
})
return () => {
animations.forEach((a) => a.cancel())
copies.forEach((copy) => copy.remove())
}
}
function paths(svg) {
return [...svg.querySelectorAll('path, line, polyline, polygon, circle, ellipse, rect')].filter(
(el) => !el.closest('defs, clipPath, mask'),
)
}
pulseRing(document.querySelector('.k-pulse-ring'))