The stroke draws itself on, as if being written. Measures each path and animates its dash offset to zero.
Every animation on the page redraws with the shape you pick.
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-line-draw" 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 lineDraw(svg) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 1
const _stagger = _reducedMotion ? 0 : 0.15
const _delay = _reducedMotion ? 0 : 0
const shapes = paths(svg)
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)'
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 }, { strokeDashoffset: 0 }], {
duration: _duration * 1000,
delay: _delay * 1000 + i * _stagger * 1000,
easing,
fill: 'both',
})
// Once drawn, drop the dash: a resize after this point would otherwise
// leave a screen-pixel dash that no longer covers the path.
animation.finished.then(() => (shape.style.strokeDasharray = 'none')).catch(() => {})
return animation
})
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'),
)
}
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
}
lineDraw(document.querySelector('.k-line-draw'))