The box at the bottom of a form where somebody signs with a finger, a mouse or a stylus — and, beside it, the field where somebody who cannot draw types their name instead. Reach for it wherever a screen asks for assent that is meant to bind: a delivery or handover receipt, a rental or equipment checkout, a treatment or research consent form, a waiver and liability release, a visitor or contractor sign-in at a front desk, a timesheet or job-completion sheet a technician gets signed on a tablet, a lease or invoice approval, a school permission slip, and the light end of e-signature where a full contract platform is far more than the job needs. Common asks it answers: "signature pad react", "react signature canvas", "draw signature component", "e-signature input react", "sign here box react", "capture signature on tablet", "signature-pad alternative", "react-signature-canvas alternative", "shadcn signature pad", "shadcn signature input", "canvas drawing react component", "why is my canvas blurry", "canvas blurry on retina", "devicePixelRatio canvas react", "canvas high dpi scaling", "smooth line drawing canvas", "canvas drawing looks jagged", "pointermove skipping points", "getCoalescedEvents react", "line breaks when mouse leaves canvas", "setPointerCapture drawing", "canvas drawing scrolls page on mobile", "touch-action none canvas", "signature to png data url", "export canvas as svg", "trim whitespace around signature", "transparent signature png invisible", "accessible signature field", "signature pad screen reader", "canvas accessibility alternative", "署名 パッド react", "サイン 入力 canvas", "canvas がぼやける retina". Official shadcn/ui has nothing to build this from, and the measurement is not close: fetching all sixty-three registry entries today (sixty-two are fetchable — questionnaire is listed and 404s on both style tracks) and grepping 240 KB of source, getContext, toDataURL, toBlob, pointerdown, pointermove, setPointerCapture, getCoalescedEvents, devicePixelRatio, beginPath, lineTo, quadraticCurveTo, touch-action and signature are every one of them a zero hit. The only match for canvas at all is ten occurrences in sidebar, and every one is the Tailwind variant name offcanvas. There is no component in official shadcn that draws — not one — so an agent asked for a signature field builds it from a bare canvas and a mousemove listener, and the four things that make this hard are exactly the four it will get wrong. The first is that a canvas has two sizes and the wrong one is the obvious one. The CSS size is how big the element looks; the width and height attributes are how many pixels actually exist, and they stay at 300x150 no matter what the stylesheet says. Left alone, a phone at 3x renders the box at a third of its own resolution and scales the result up, so the strokes come out soft — and a signature is a thin line whose weight and wobble are the whole of what identifies it, which makes this the one component where blur is not cosmetic. The attributes are set to the CSS size times the device pixel ratio, capped so a 4x screen does not allocate sixteen pixels of memory per CSS pixel, and the context is set back to CSS coordinates with setTransform rather than scale — scale multiplies into the transform already there, so a pad that survives two resizes draws at 4x and the signature walks off the box. The second is that pointermove is not the pointer. One move event is delivered per animation frame, but the digitiser sampled the pen many times inside that frame and the browser keeps the ones it skipped. A quick signature is where it shows: at 60Hz a fast flick is four or five points and comes out as a zigzag, while getCoalescedEvents returns the twenty that were really seen. Safari has been observed returning an empty list, so the event itself is the fallback rather than the assumption. Sampling alone is still not enough, because the corners in a joined-up polyline are in the sample rate rather than in the hand: each sample becomes the control point of a quadratic and the curve runs through the midpoints between them, so the line is tangent to the path the hand took and has no corners of its own. During a stroke only the newest segment is drawn, so a long signature costs the same per sample as a short one. The third is that a canvas is a blank to a screen reader, and no aria-label fixes it — the label names the box, and the task is to make a mark inside it. Drawing is a pointer gesture, so a pad that only draws is a form that a keyboard, switch or screen reader user cannot complete, on precisely the documents where being unable to complete it has consequences. A typed full name is the equivalent that is already recognised in practice, so it sits beside the box as a real labelled field rather than as a fallback bolted on: either path produces a value and an image, typing renders in a script face on the canvas so a sighted user sees the mark too, and the two are mutually exclusive because there is one signature. required is handled the same way round: it goes on the typed field while nothing is signed and lifts the moment something is drawn — never on the hidden input, which is barred from constraint validation outright, so the attribute parses, the browser ignores it, and the form submits unsigned. The fourth is that the line breaks when the hand leaves the box, which is where the descender of a real signature goes. setPointerCapture keeps the events coming until the pointer lifts, wherever that happens; the capture is checked before being released, because releasing one that pointercancel already took throws; a second finger landing mid-stroke is ignored rather than allowed to overwrite the stroke in progress; and touch-action is none, or the first downward stroke on a phone scrolls the page instead of drawing while the browser waits to find out which was meant. Strokes are kept as data, not as pixels, which is what makes the rest work. Undo is a slice. A resize — a sidebar opening, a tab becoming visible, a container query firing, none of them a window resize — is observed with a ResizeObserver and replayed, where a pixel-only pad loses the signature to the attribute assignment that resizes it. And the export is resolution-independent: signatureToSvg emits a real SVG document, because a signature is stored small and shown large, printed onto a contract or scaled into a PDF, and a raster of a 160-pixel box has one resolution forever. trim crops to the ink so a stored signature is not mostly empty box, with the bound taken over the Bézier control points — a quadratic stays inside the triangle of its own three, so it is exact without solving the curve — plus half the pen width, without which the trim slices the outermost stroke in half. toDataURL is there for the APIs that want a raster, and its background is documented rather than assumed: a transparent PNG of black ink is invisible the moment it lands on anything dark. Every string that reaches the SVG is escaped, since a name is free text and a document built by concatenation is the oldest bug there is. The API: value or defaultValue as a discriminated { type: "drawn", strokes, width, height } or { type: "typed", name } — the drawn form carries the box it was drawn in, because strokes are CSS pixels and without the box they cannot be laid out again on the receipt screen that shows them later. Plus onChange, penColor which defaults to the theme's own text colour, penWidth, height, maxPixelRatio, disabled, allowTyped, required, and name to post the signature through a plain HTML form as an SVG data URL. The ref exposes clear, undo, isEmpty, getValue, toSVG and toDataURL for a submit handler. strokeGeometry, strokePathData, signatureBounds, isSignatureEmpty, signatureToSvg and backingSize are exported as pure functions, so a stored signature can be rendered on a server that has no canvas at all. Within pulld it is the first component that draws: file-dropzone takes an image in and upload-list lists what arrived, but nothing until now made a mark. It sits next to type-to-confirm, which is the other way a screen asks somebody to mean it — typing a phrase to authorise a destructive action, where this captures assent that gets stored and shown back. One file, zero dependencies, not even an icon, and every colour is a shadcn token, so light and dark follow on their own.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/signature-pad.json"