I needed a signature pad for RapidKit Tools, a set of free browser-based tools I'm building. Most online signature tools upload the image to a server. A signature is personal, so I wanted the drawing to stay on the user's device: no upload, no account.

I didn't write the drawing engine from scratch. The tool is built on react-signature-canvas, a React wrapper around signature_pad. Reading through the source taught me more than I expected, so here is what's actually going on under the hood.

Smooth lines are curves, not lineTo

Connecting raw pointer coordinates with straight segments looks jagged. signature_pad buffers recent points, computes control points from them, and draws cubic Bézier curves. It also varies the stroke width with pointer velocity (faster movement gives a thinner line), and it rasterizes each curve by stepping along it and drawing small filled circles.

High-DPI canvases

The wrapper sizes the canvas bitmap to offsetWidth * devicePixelRatio and scales the 2D context to match, so strokes stay sharp on phone screens. If you build your own canvas tool, forgetting this step is the classic cause of blurry output.

Transparent, cropped PNGs

Exporting a signature as JPEG gives you a white box when it's pasted into a document. The canvas here has no background fill, so toDataURL("image/png") keeps the alpha channel. trim-canvas then scans the pixels and crops the image to the bounding box of what was actually drawn.

The bug: resizing wiped the signature

By default, the wrapper clears the canvas whenever the window resizes. On mobile, a resize happens when the browser's URL bar collapses or when you rotate the screen. A signature could silently disappear while my React state still said "not empty", so the Copy and Download buttons stayed enabled and did nothing.

What I changed:

  1. Set clearOnResize={false}.
  2. On resize, snapshot the strokes with toData(), resize the backing store, rescale the points, and restore them with fromData().
  3. Derive the "empty" state from the canvas itself (isEmpty()) instead of a flag I set by hand.
  4. For the clipboard, use canvas.toBlob() and hand the blob to ClipboardItem, and show an error state if copying fails instead of only logging to the console.

What I got wrong

I described the code before reading it. My first draft of this post, written with AI help, said the tool used Pointer Events and quadratic curves. That was a plausible guess about how a signature pad works, not a description of this one. When I read the library's source, it turned out to use mouse and touch events and cubic Béziers. I rewrote the post after reading the code.

I only checked the first version on desktop. It looked fine in my browser. On my phone, the drawing area was a narrow pill: stacked paddings and large border radii ate the width, strokes were clipped near the rounded edges, and the "Copy to Clip" label wrapped onto three lines. I fixed it with a taller mobile aspect ratio, smaller radii, and full-width stacked buttons, then checked it on the phone again.

I trusted a local flag as the source of truth. The tool tracked "is the canvas empty" in a React state variable set when drawing started. Reading the code showed the problem: the library clears the canvas on resize, but my flag would still say "not empty", so Copy and Download stayed enabled with nothing to export. I found that by having an AI coding agent report what the component does, with file and line references, before I changed anything. The fix was to ask the canvas itself (isEmpty()) instead of keeping a copy of its state.

Limits worth knowing

  • The library uses mouse and touch events, not Pointer Events, so there's no stylus pressure support.
  • The export is a raster PNG, not SVG.
  • The signature itself is never sent anywhere. The site does load Google Analytics for page views, so I only make that claim about the drawing.

Try it

getrapidkit.me/tools/design/signature-pad is free, with no sign-up. If you've built canvas tools, I'd like to hear how you handle resize and smoothing, and whether you hit problems I haven't found yet.