One long stroke written at the pace of a real signature: slower than it feels it should be, and eased in and out the way a hand gathers speed mid-stroke.
Every animation on the page redraws with the shape you pick.
<!-- 1. Markup -->
<svg viewBox="0 0 120 120" class="k-signature-write" 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 signatureWrite(svg) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 2.4
const _delay = _reducedMotion ? 0 : 0
const [stroke] = paths(svg).filter((s) => s.getAttribute('stroke'))
if (!stroke) {
return () => {}
}
const length = strokeLength(stroke)
const pad = capPad(stroke)
const hidden = length + pad
stroke.style.strokeDasharray = `${length} ${hidden + pad}`
const animation = stroke.animate([{ strokeDashoffset: hidden }, { strokeDashoffset: 0 }], {
duration: _duration * 1000,
delay: _delay * 1000,
easing: 'linear(0.0000,0.0013,0.0053,0.0118,0.0210,0.0329,0.0473,0.0644,0.0842,0.1065,0.1315,0.1591,0.1893,0.2222,0.2577,0.2959,0.3366,0.3800,0.4260,0.4747,0.5253,0.5740,0.6200,0.6634,0.7041,0.7423,0.7778,0.8107,0.8409,0.8685,0.8935,0.9158,0.9356,0.9527,0.9671,0.9790,0.9882,0.9947,0.9987,1.0000)',
fill: 'both',
})
// Once written, drop the dash so a later resize can't reopen a gap.
animation.finished.then(() => (stroke.style.strokeDasharray = 'none')).catch(() => {})
return () => {
animation.cancel()
stroke.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
}
signatureWrite(document.querySelector('.k-signature-write'))