Marching ants: a fixed dash pattern crawls along every stroke, forever. Under a non-scaling stroke the pattern is in screen pixels, so the dashes keep their size at any scale.
Every animation on the page redraws with the shape you pick.
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-dash-march" 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 dashMarch(svg) {
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return () => {}
const shapes = paths(svg).filter((s) => s.getAttribute('stroke'))
// One dash and one gap. The gap outruns a round cap's overhang on an 8px
// stroke, which would otherwise close it up into a solid line.
const dash = 8
const gap = 14
const animations = shapes.map((shape) => {
shape.style.strokeDasharray = `${dash} ${gap}`
return shape.animate([{ strokeDashoffset: 0 }, { strokeDashoffset: -(dash + gap) }], {
duration: 0.8 * 1000,
iterations: Infinity,
easing: 'linear',
})
})
return () => {
animations.forEach((a) => a.cancel())
shapes.forEach((shape) => {
shape.style.strokeDasharray = ''
})
}
}
function paths(svg) {
return [...svg.querySelectorAll('path, line, polyline, polygon, circle, ellipse, rect')].filter(
(el) => !el.closest('defs, clipPath, mask'),
)
}
dashMarch(document.querySelector('.k-dash-march'))