# Signature Pad (/docs/inputs/signature-pad)



<ComponentShowcase name="signature-pad" />

## Installation [#installation]

```bash
npx shadcn@latest add https://ui.zyeon.ai/r/signature-pad.json
```

## Prompt [#prompt]

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

```text
Build a React + TypeScript + Tailwind "SignaturePad" component (lucide-react
Eraser/Undo2, native <canvas>, Pointer Events — no signature-drawing library).

Contract
- Export a forwardRef<HTMLDivElement, SignaturePadProps> extending
  HTMLAttributes<HTMLDivElement> (omit native onChange).
- Props: onChange? (dataUrl: string | null => void) — fires after every
  completed stroke with a PNG data URL, and with null after Clear or once
  Undo empties the history; height (default 160); strokeWidth (default 2);
  disabled; className.
- No imperative ref API: the forwarded ref is the plain container div: Undo
  and Clear are built-in buttons, not exposed methods.

Behavior
- The canvas fills the container's width. A ResizeObserver on the wrapping
  div re-measures width, resizes the canvas's backing store
  (devicePixelRatio-scaled) and repaints every stored stroke — the pad
  adapts to width changes without stretching or blurring.
- Drawing uses Pointer Events, not mouse/touch events: pointerdown starts a
  stroke and calls setPointerCapture so the drag stays tracked even once the
  pointer strays past the canvas edge; pointermove appends points and draws
  an incremental line segment; pointerup/pointercancel end the stroke —
  never pointerleave, which would fire mid-drag under capture and cut a
  large signature short.
- Strokes are kept as an array of point arrays, not just painted pixels, so
  Undo can pop the last stroke and repaint everything from that array, and
  so a resize can repaint at the new backing-store size. A stroke that never
  moved (a tap) still paints a filled dot, so a signature can include an "i"
  dot or a period.
- Ink color is read from getComputedStyle(container).color (the container
  carries text-foreground) at the start of every new stroke, so a theme
  toggle recolors starting with the very next stroke. Existing strokes
  recolor to whatever the current color is whenever a repaint runs (resize,
  Undo, Clear) — documented simplification, strokes don't remember the
  color they were originally drawn in.
- Undo removes the last stroke and re-emits onChange (a fresh data URL, or
  null once nothing is left); Clear wipes every stroke and calls
  onChange(null). Both buttons disable themselves when there's nothing to
  undo/clear.
- An empty state shows a centered "Sign here" placeholder until the first
  stroke, then it disappears.
- disabled stops all drawing and disables both buttons; the whole control
  dims via reduced opacity.
- touch-action: none on the canvas stops the page from scrolling while
  drawing on a touch device. The ResizeObserver disconnects on unmount.

Rendering & styling
- Semantic tokens only: bg-background / text-foreground / border on the
  container, bg-muted/30 + border-t on the toolbar row, text-muted-foreground
  for the placeholder and icon buttons. Merge className via cn().
- The canvas is role="img" aria-label="Signature pad" — drawing is
  inherently pointer-only, so it is not exposed as a focusable keyboard
  control; Undo and Clear are the two real, labelled, keyboard-reachable
  <button>s, each with aria-label and a focus-visible ring.

Customization levers
- Height: the height prop (canvas height in px; width always fills the
  container).
- Stroke weight: the strokeWidth prop.
- Ink color: swap text-foreground on the container for another token (e.g.
  text-chart-2) — the pad always draws in the container's computed color,
  so this alone recolors ink.
- Toolbar: swap Eraser/Undo2 for other lucide icons, or reposition the
  toolbar div.
- Known limitation: the backing store re-rasterizes on container WIDTH
  changes; a DPR-only change (dragging the window to a different-density
  display without a width change) keeps the old resolution until the next
  resize or stroke — add a matchMedia("(resolution: …dppx)") listener if
  that matters to you.
- Not built in (would need real changes, not just props): export formats
  other than PNG (re-implement the stroke renderer to trace SVG paths
  instead of canvas lines); pressure-sensitive strokes (read e.pressure on
  each pointer event and vary lineWidth per segment); a fixed, non-token ink
  color (bypass the getComputedStyle read entirely).
```

## Concepts [#concepts]

<Mermaid
  chart="`flowchart TD
A[&#x22;pointerdown&#x22;] --> B[&#x22;setPointerCapture +<br/>read foreground color&#x22;]
B --> C[&#x22;pointermove appends points<br/>+ draws incremental segment&#x22;]
C --> D[&#x22;pointerup / pointercancel&#x22;]
D --> E[&#x22;push stroke into history&#x22;]
E --> F[&#x22;onChange(PNG data URL)&#x22;]
G[&#x22;ResizeObserver width change&#x22;] --> H[&#x22;resize canvas backing store<br/>(devicePixelRatio-scaled)&#x22;]
H --> I[&#x22;repaint every stored stroke<br/>in current foreground color&#x22;]
J[&#x22;Undo click&#x22;] --> K[&#x22;pop last stroke&#x22;]
K --> I
K --> F
L[&#x22;Clear click&#x22;] --> M[&#x22;empty history&#x22;]
M --> I
M --> N[&#x22;onChange(null)&#x22;]`"
/>

* **Pointer capture, not boundary events** — pointerdown calls `setPointerCapture` so drag events keep targeting the canvas even once the cursor strays past its edge; only pointerup/pointercancel end a stroke, never pointerleave.
* **Stroke history as data, not pixels** — every stroke is stored as an array of points, not just painted ink, so Undo and resize can both repaint from that array instead of only ever adding to the canvas.
* **Resize-aware backing store** — a ResizeObserver on the wrapping element resizes the canvas's actual pixel buffer (devicePixelRatio-scaled) whenever the container's width changes, then repaints the stored strokes so lines stay crisp instead of stretching.
* **Foreground-token ink** — stroke color is read from the container's computed `color` (which carries `text-foreground`) at the start of every new stroke, so switching themes recolors ink starting with the very next stroke.
* **Tap-to-dot** — a stroke that never moved still paints a filled dot at that point, matching how a real pen leaves a mark on a tap.
