A clip rectangle sweeps across, uncovering the artwork left to right. The clip is built in JS, so the markup stays plain art.
Every animation on the page redraws with the shape you pick.
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-clip-wipe" 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 clipWipe(svg) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 0.8
const _delay = _reducedMotion ? 0 : 0
const NS = 'http://www.w3.org/2000/svg'
const box = svg.viewBox.baseVal
const id = `wipe-${Math.random().toString(36).slice(2, 9)}`
const defs = document.createElementNS(NS, 'defs')
const clip = document.createElementNS(NS, 'clipPath')
clip.setAttribute('id', id)
const rect = document.createElementNS(NS, 'rect')
rect.setAttribute('x', box.x)
rect.setAttribute('y', box.y)
rect.setAttribute('width', box.width)
rect.setAttribute('height', box.height)
// Scaled rather than resized: x/y/width/height only became animatable CSS
// properties in Safari 16.4, while a transform on a clip path's contents
// has always worked, and the wipe is indistinguishable either way.
// fill-box makes "left" the rect's own left edge. Without it "left" is the
// viewBox's, which only coincides while the viewBox happens to start at 0.
rect.style.transformBox = 'fill-box'
rect.style.transformOrigin = 'left'
clip.appendChild(rect)
defs.appendChild(clip)
const art = [...svg.children]
art.forEach((el) => el.setAttribute('clip-path', `url(#${id})`))
svg.appendChild(defs)
const animation = rect.animate([{ transform: 'scaleX(0)' }, { transform: 'scaleX(1)' }], {
duration: _duration * 1000,
delay: _delay * 1000,
easing: 'linear(0.0000,0.0750,0.1461,0.2135,0.2772,0.3374,0.3942,0.4476,0.4978,0.5448,0.5889,0.6299,0.6682,0.7037,0.7366,0.7670,0.7949,0.8205,0.8439,0.8651,0.8844,0.9017,0.9172,0.9309,0.9431,0.9537,0.9630,0.9709,0.9776,0.9831,0.9877,0.9914,0.9942,0.9964,0.9979,0.9989,0.9995,0.9999,1.0000,1.0000)',
fill: 'both',
})
return () => {
animation.cancel()
art.forEach((el) => el.removeAttribute('clip-path'))
defs.remove()
}
}
clipWipe(document.querySelector('.k-clip-wipe'))