Signature Write

draw

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.

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