← All notes

Drawing a signature with GSAP DrawSVG

How the signature on this site writes itself: font outlines from opentype.js, a stroked mask, and a pen that moves at an even speed.

31 Oct 20262 min readGSAP, SVG, Animation

The first thing you see on this site is my name writing itself. It isn't a video or a hand-traced path. It is a real font, turned into SVG and revealed by GSAP's DrawSVG plugin.

Live demo: the same component the preloader uses

Why not just animate the text?

A script font looks hand-written, but the browser draws each glyph in one go. To make it look like a pen is moving, you need the glyph's path, so you can reveal it a little at a time.

Step 1: turn the font into paths

opentype.js reads a font file in the browser and gives you each glyph's outline as SVG path data.

signature.tsx
const font = opentype.parse(await (await fetch(FONT_URL)).arrayBuffer())
 
let x = padding
const paths = [...text].map((char) => {
  const glyph = font.charToGlyph(char)
  const d = glyph.getPath(x, baseline, fontSize).toPathData(3)
  x += glyph.advanceWidth * (fontSize / font.unitsPerEm)
  return d
})

One path per letter keeps the animation in your hands: you can time every letter on its own.

Step 2: reveal the fill with a stroked mask

A font glyph is an outline. If you draw its stroke, you get a thin line tracing the edge of each letter, which looks nothing like ink.

The trick is to draw the outline as a thick white stroke inside a mask, and put the solid glyph underneath. As the stroke draws, it uncovers the fill.

<mask id={maskId} maskUnits="userSpaceOnUse">
  {paths.map((d, i) => (
    <path key={i} d={d} fill="none" stroke="white" strokeWidth={fontSize * 0.18} />
  ))}
</mask>
 
<g mask={`url(#${maskId})`}>
  {paths.map((d, i) => <path key={i} d={d} fill={color} />)}
</g>

Step 3: one pen, even speed

Animating every letter with the same duration and a stagger looks mechanical: a short i and a long M take the same time.

A real pen moves at a steady speed, so each letter gets time in proportion to its length:

const lengths = strokes.map((el) => el.getTotalLength())
const total = lengths.reduce((sum, l) => sum + l, 0)
 
const pen = gsap.timeline({ paused: true })
let cursor = 0
strokes.forEach((el, i) => {
  const span = lengths[i] / total
  pen.to(el, { drawSVG: "100%", duration: span, ease: "sine.inOut" }, cursor)
  cursor += span
})

sine.inOut on each letter gives a small slow-down at every pen lift. Then one outer tween drives the whole thing, with its own ease, so the signature starts with a flick and settles at the end:

gsap.to(pen, { progress: 1, duration: 1.2, ease: "power2.inOut" })

Step 4: a nib on the tip

A small dot that follows the tip sells the illusion. On every update, find the letter being drawn and ask the path where its tip is:

const point = strokes[i].getPointAtLength(local * lengths[i])
nib.setAttribute("cx", String(point.x))
nib.setAttribute("cy", String(point.y))

Wrapping up

The whole effect is four ideas: real glyph paths, a stroked mask, time weighted by length, and a nib. None of them is heavy, and the signature stays crisp at any size because it is still a font.

Need this on your site?

I build interfaces like this for agencies, startups and SaaS teams.

Let's talk