GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

Signature Pad JavaScript: 9 Canvas Drawing UIs

These signature pad javascript UIs use only the browser's canvas and Pointer Events — no library.

Auto-plays · click a tile to jump to its section · all nine in one zip

The nine signature pad js examples are ordered not by flashiness but by the life of one signature. First, the drawing trio — a basic pad whose width follows pressure or speed (01), a toolbar that swaps pen, highlighter, and eraser (02), and a shape board where one drag snaps lines, rectangles, and circles into place (03). Next, the precision trio — a grid-snap board for layout sketches (04), a two-layer board that inks over a faded sketch (05), and a replay that redraws your strokes in the order you drew them (06). Last, the outbound trio — an export that trims its own margins into a transparent PNG (07), a UI that seats large handwriting in a small form field (08), and a contract field that locks on confirm (09). Stage colors run two paper, three ink, two yellow, and two orange, so the nine grid cells don't lean one way. For the wider story of hands driving screens, see Mobile Touch Gesture UI: 9 Patterns; for canvas as a zoom tool, see Image Zoom Viewer UI: 9 Patterns. Drive the same canvas with boxes and wires instead of freehand strokes and it becomes node based UI design; point it at sound and it becomes the audio visualizer.

01Pressure-aware signature pad

What this signature pad js shows

The basic pad that sets stroke width from the Pointer Events pressure value. Devices without pressure report a flat 0.5, so width falls back to the distance from the previous point — drawing speed.

Pressure-aware signature pad
Open in a new tab

01 · Pressure-aware signature pad — Measured area 3.913% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

pointerdownlineWidthquadraticCurveTo

Where to look in the code

Its preview, where the signature rewinds as if undo had been pressed three times, measured 3.913% cumulative area with 22 of 23 frames moving; it fits delivery-receipt signatures and mobile consent forms.

// With real pressure use it; otherwise derive width from distance (speed) — faster is thinner
function widthFor(point) {
  if (point.pressure > 0 && point.pressure !== 0.5) { return 1.2 + point.pressure * 5.2; }
  var dx = point.x - lastPoint.x, dy = point.y - lastPoint.y;
  return Math.max(1.4, 5.4 - Math.sqrt(dx * dx + dy * dy) * 0.34);
}
// Segments run midpoint to midpoint with the joint as the control point.
// Start each segment at the raw point and the gap between point and midpoint stays empty — the stroke comes out half-dashed
var start = pts[0];
for (var i = 1; i < pts.length - 1; i++) {
  var end = midOf(pts[i], pts[i + 1]);
  ctx.lineWidth = pts[i].w;
  ctx.beginPath();
  ctx.moveTo(start.x, start.y);
  ctx.quadraticCurveTo(pts[i].x, pts[i].y, end.x, end.y);
  ctx.stroke();
  start = end;
}

02Pen, highlighter, eraser toolbar

What happens on screen

A drawing board where three tools differ by exactly one thing: the composite mode. The pen paints with source-over, the highlighter uses multiply so the text beneath shows through, and the eraser carves alpha away with destination-out.

Pen, highlighter, eraser toolbar
Open in a new tab

02 · Pen, highlighter, eraser toolbar — Measured area 8.903% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

globalCompositeOperationdestination-outmultiply

How to read the code

The preview loop, whose eraser sweeps across in a slow wipe, moves in all 23 of 23 frames with 8.903% cumulative area and 135.5 strength; it fits screen-annotation tools and markup boards over teaching material.

// The only thing that changes per tool is globalCompositeOperation.
// pen         source-over     — paints on top of what is there
// highlighter multiply        — multiplies instead of covering, so text and pen strokes show through the band
// eraser      destination-out — instead of painting, it shaves alpha off existing pixels
var TOOLS = {
  pen: { mode: 'source-over', color: null, width: null },
  highlighter: { mode: 'multiply', color: '#ffd23f', width: 16 },
  eraser: { mode: 'destination-out', color: '#000000', width: 20 }
};

03Shape snap drawing

What moves

A shape board where a dashed preview follows the drag and commits to a solid shape on release. Each pointermove clears the board, redraws the committed shapes, and then draws only the dragged one with setLineDash.

Shape snap drawing
Open in a new tab

03 · Shape snap drawing — Measured area 8.56% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

strokeRectarcclearRect

What to change

The frame where a thick line shoots out in one fast span became the poster, at 8.56% cumulative area; it fits boxing screenshots and drawing dimension lines over plans.

function toLocal(e) {
  var r = ink.getBoundingClientRect();
  return {
    x: Math.round((e.clientX - r.left) / SNAP) * SNAP,
    y: Math.round((e.clientY - r.top) / SNAP) * SNAP
  };
}
// While dragging: dashed preview. On release: committed solid line
ctx.setLineDash([6, 5]);

04Grid-snap canvas

What you see

A layout board whose ruled background is two CSS repeating-linear-gradient layers, with one Math.round snapping each click to the nearest intersection. The grid is fixed at 12 by 4 cells, so the rounding formula never drifts from the painted lines.

Grid-snap canvas
Open in a new tab

04 · Grid-snap canvas — Measured area 5.368% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

repeating-linear-gradientMath.roundaria-pressed

Where to look in the code

Its preview, where snap rings pop at each bend, measured 5.368% cumulative area with 21 of 23 frames moving; it fits quick layout sketches and seating charts.

function stepSize() {
  var r = board.getBoundingClientRect();
  return { x: r.width / COLS, y: r.height / ROWS };
}
function snapPoint(p) {
  if (!snap) { return p; }
  var s = stepSize();
  return { x: Math.round(p.x / s.x) * s.x, y: Math.round(p.y / s.y) * s.y };
}

05Sketch and ink layers

What happens on screen

A trace-along board that splits sketch from ink across two stacked canvases. The sketch is drawn with lowered globalAlpha, the slider drives the sketch layer's CSS opacity, and Merge presses the sketch into the ink layer with drawImage.

Sketch and ink layers
Open in a new tab

05 · Sketch and ink layers — Measured area 4.01% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

globalAlphadrawImageopacity

How to read the code

Its loop — the sketch slowly fading, then the ink rewinding — measured 4.01% cumulative area; it fits trace-along learning tools and inking over templates.

// Merge — press the sketch into the ink layer at its current strength, then clear the sketch layer
inkCtx.save();
inkCtx.setTransform(1, 0, 0, 1, 0, 0);
inkCtx.globalAlpha = Number(sketch.style.opacity || 0.34);
inkCtx.drawImage(sketch, 0, 0);
inkCtx.restore();
sketchCtx.clearRect(0, 0, sketch.width, sketch.height);

06Stroke order replay

What moves

The board that remembers the order you drew in and redraws it from the start. It converts each stroke's point array into an SVG path d, measures it with getTotalLength(), sets that length as the stroke-dasharray, and then runs stroke-dashoffset to zero so the stroke draws itself — the same technique the preview uses, which is why every chip is real.

Stroke order replay
Open in a new tab

06 · Stroke order replay — Measured area 5.949% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

stroke-dasharraystroke-dashoffsetgetTotalLength

What to change

The near-finished signature became the poster at 5.949% cumulative area and 164.2 strength; it fits signature verification screens and stroke-order learning content.

// Point array to SVG path d — the same curve the canvas drew
function toPathData(pts) {
  var d = 'M' + pts[0].x.toFixed(1) + ',' + pts[0].y.toFixed(1);
  for (var i = 1; i < pts.length; i++) {
    d += ' Q' + pts[i - 1].x.toFixed(1) + ',' + pts[i - 1].y.toFixed(1) +
         ' ' + ((pts[i].x + pts[i - 1].x) / 2).toFixed(1) + ',' + ((pts[i].y + pts[i - 1].y) / 2).toFixed(1);
  }
  return d;
}
// Measure the length, hide the line behind one dash of that length, then slide the dash away
var len = path.getTotalLength();
var ms = Math.max(240, Math.round(len * 2.4));
path.style.setProperty('stroke-dasharray', len + 'px');
path.style.setProperty('stroke-dashoffset', len + 'px');
path.style.setProperty('transition', 'stroke-dashoffset ' + ms + 'ms linear ' + delay + 'ms');

07Auto-crop export

What you see

An export that scans the alpha channel with getImageData to find the ink's bounding box, copies just that region into a scratch canvas with the 9-argument drawImage, and produces a transparent-background PNG with toDataURL. Because the background is never painted, alpha survives the crop.

Auto-crop export
Open in a new tab

07 · Auto-crop export — Measured area 5.496% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

getImageDatatoDataURLdrawImage

Where to look in the code

The frame where the result pops into the checkerboard cell became the poster at 5.474% cumulative area; it fits attaching signature images and making seal images for documents.

// The background was never painted, so empty spots have alpha 0 — scanning alpha alone yields the bounding box
if (data[(y * ink.width + x) * 4 + 3] > 8) {
  if (x < minX) { minX = x; }
  if (x > maxX) { maxX = x; }
  if (y < minY) { minY = y; }
  if (y > maxY) { maxY = y; }
}
// Only the cropped region is copied, unpainted — the alpha survives, so the PNG keeps a transparent background
cropped.getContext('2d').drawImage(ink, minX, minY, sw, sh, 0, 0, sw, sh);
out.src = cropped.toDataURL('image/png');

08Handwriting fitted to the input

What happens on screen

A UI that seats large handwriting into a small form field. It measures the target field with getBoundingClientRect(), computes scaleValue = (slotRect.height * 0.72) / inkHeight, and scales the pad down about a transform-origin: left center axis — the origin sits left because handwriting must land on the field's left baseline.

Handwriting fitted to the input
Open in a new tab

08 · Handwriting fitted to the input — Measured area 7.096% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

getBoundingClientRecttransform-originscale

How to read the code

The frame where the whole scribble shrinks into the field measured 7.096% cumulative area; it fits handwritten name fields on applications and signature fields on account forms.

// Measure the target field instead of guessing — it keeps up when font or line-height changes
var slotRect = slot.getBoundingClientRect(), padRect = pad.getBoundingClientRect();
var inkHeight = Math.max(8, box.bottom - box.top);
var scaleValue = (slotRect.height * 0.72) / inkHeight;
// transform-origin is left center — handwriting must land on the field's left baseline, so the axis is the left edge, not the center
wrap.style.transform = 'translate(' + tx.toFixed(1) + 'px, ' + ty.toFixed(1) + 'px) scale(' + scaleValue.toFixed(3) + ')';

09Contract signature field lock

What moves

The final confirm screen of an e-contract: pressing confirm locks the field with pointer-events: none, and a cover drops over the pad with a lock at its left edge and a time stamp from toLocaleTimeString() punched into the middle. That stamping cut gives the episode its biggest number, 16.999% cumulative area.

Contract signature field lock
Open in a new tab

09 · Contract signature field lock — Measured area 16.999% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

aria-livepointer-eventstoLocaleTimeString

What to change

It fits the last confirmation step of e-contracts and the screen right before a consent form is submitted.

function confirmSign() {
  // The lock is one class — the pad becomes pointer-events: none, so nothing more can be drawn on it
  root.classList.add('is-locked');
  stampTime.textContent = new Date().toLocaleTimeString();
  live.textContent = DONE;
}
.cl.is-locked .cl__pad { pointer-events: none; }

Where it breaks — the trap

The first trap is a stroke that looks half-dashed. If you open each segment with moveTo(point) and hand quadraticCurveTo the next point as its control point, the curve bends toward that control point and ends at the midpoint of the two points; the next segment then starts back at the raw point, so the gap between midpoint and point is blank every time. On this episode's item 01, a Playwright drag with twelve bends left 1,952 ink pixels in eight disconnected slants before the fix, and 1,933 pixels in one continuous line after moving both ends of every segment to midpoints (evidence: run/287/_수리전실측.json). The second shows up the moment you omit touch-action: finger drawing fights the page scroll, so the drawing surface has to declare touch-action: none. The third is strokes blurring on high-density screens, fixed by scaling the backing store by devicePixelRatio and restoring the coordinate system with setTransform. All nine items ship with these three fixes already in place.

Trap Symptom Fix
Segment starts at the raw point Stroke half-dashed, jagged Join midpoint to midpoint
No touch-action Finger drawing fights scroll touch-action: none on the surface
1x backing store Blurry on retina devicePixelRatio scale + setTransform

Handing the confirmed signature in 09 straight to a machine step afterward is what the n8n workflow examples pick up. The archive password for the nine files is jjyvftnq; unpacking it gives you one vanilla and one React version for each item.

Accessibility (reduced-motion)

To a screen reader a canvas is just a picture, so all nine carry role="img" with an aria-label, and the five boards that have something to count keep that label counting ("Signature box, 3 strokes"). Tool, shape, and layer switching are real buttons with aria-pressed, and seven of the nine report their results through an aria-live="polite" region. Every control is a real <button>, so tab reaches the tools and Enter fires them, confirm included. Under prefers-reduced-motion the preview loops stop and leave the drawn result standing — the replay (06) jumps straight to the finished signature.

FAQ

Do I really need pressure on a signature pad online?

No. A mouse reports pressure as a fixed 0.5 while pressed, so item 01 reads that as "no pressure" and switches to speed-based width. Pressure-driven width only kicks in on inputs that report real force, such as a stylus or a drawing tablet (PointerEvent.pressure docs). If the work is editing an uploaded photo rather than a drawn mark, image crop editor UI uses the same canvas as a cutting tool.

Do Pointer Events work in Safari on iPhone?

Yes. Pointer Events have been in iOS Safari since 13.2, with about 97% global support (caniuse). Just don't forget touch-action: none on the drawing surface so the page doesn't scroll the moment a finger starts drawing.

How do I send the signature image to a server?

Send the string that 07's toDataURL('image/png') produces. Turn the data URL into a Blob with fetch, or POST it as a hidden input's value — the margin is already cropped off, so the payload stays small. If the form needs input validation, you can layer the rules from Form Validation UI: 9 Patterns on top.

Enter the archive password

The password is inside this article. You will find it as you read.