Clip Wipe

reveal

A clip rectangle sweeps across, uncovering the artwork left to right. The clip is built in JS, so the markup stays plain art.

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-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'))