Draws on, holds, then the tail chases the head off the far end. The dash offset runs on past zero to minus the path length, so the line leaves in the direction it was drawn.
Every animation on the page redraws with the shape you pick.
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-draw-erase" aria-hidden="true">
<path d="M14 80 C 26 46, 36 38, 42 54 C 48 70, 39 88, 46 92 C 54 96, 66 66, 74 50
C 80 38, 90 42, 87 56 C 84 70, 73 76, 84 79 C 93 81, 101 71, 110 56"
fill="none" stroke="currentColor" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"
vector-effect="non-scaling-stroke" />
</svg>
// 2. JS
function drawErase(svg) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 1.8
const _stagger = _reducedMotion ? 0 : 0.1
const _delay = _reducedMotion ? 0 : 0
const shapes = paths(svg).filter((s) => s.getAttribute('stroke'))
const easing = 'linear(0.0000,0.0001,0.0005,0.0018,0.0043,0.0084,0.0146,0.0231,0.0345,0.0492,0.0674,0.0898,0.1165,0.1481,0.1850,0.2276,0.2762,0.3313,0.3933,0.4625,0.5375,0.6067,0.6687,0.7238,0.7724,0.8150,0.8519,0.8835,0.9102,0.9326,0.9508,0.9655,0.9769,0.9854,0.9916,0.9957,0.9982,0.9995,0.9999,1.0000)'
// Draw, hold, erase — one animation per stroke with the ease on each phase.
const animations = shapes.map((shape, i) => {
const length = strokeLength(shape)
const pad = capPad(shape)
const hidden = length + pad
shape.style.strokeDasharray = `${length} ${hidden + pad}`
const animation = shape.animate(
[
{ strokeDashoffset: hidden, easing },
{ strokeDashoffset: 0, offset: 0.42 },
{ strokeDashoffset: 0, offset: 0.58, easing },
{ strokeDashoffset: -hidden },
],
{ duration: _duration * 1000, delay: _delay * 1000 + i * _stagger * 1000, fill: 'both' },
)
// Erased is the end state. A screen-pixel dash can stop covering the path
// if the drawing is resized later, so hide it outright rather than trust it.
animation.finished.then(() => (shape.style.visibility = 'hidden')).catch(() => {})
return animation
})
return () => {
animations.forEach((a) => a.cancel())
shapes.forEach((shape) => {
shape.style.strokeDasharray = ''
shape.style.visibility = ''
})
}
}
function paths(svg) {
return [...svg.querySelectorAll('path, line, polyline, polygon, circle, ellipse, rect')].filter(
(el) => !el.closest('defs, clipPath, mask'),
)
}
function strokeLength(el) {
const m = el.getScreenCTM()
const nonScaling = getComputedStyle(el).vectorEffect === 'non-scaling-stroke'
return el.getTotalLength() * (nonScaling && m ? Math.hypot(m.a, m.b) : 1)
}
function capPad(el) {
const s = getComputedStyle(el)
return s.strokeLinecap === 'butt' ? 0 : (parseFloat(s.strokeWidth) || 0) / 2 + 1
}
drawErase(document.querySelector('.k-draw-erase'))