Signature Pad
Handwritten signatures on canvas: pressure-sensitive stroke width, clear and redraw, export to PNG.
A handwritten signature pad built with canvas 2d and Pointer Events: the line width changes live with writing speed (or with a stylus's real pressure) to imitate a fountain pen. Strokes are kept on a stack, supporting stroke-by-stroke undo, one-click clear, and PNG download, plus a dashed signature baseline and a "please sign" watermark.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add signature-pad.enSignature Pad is a React signature capture component built with canvas 2D and Motion. Drop it into a form to collect an e-signature: the result comes back as a PNG dataURL through onChange, and users can also download it as an image file.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<SignaturePad/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- The signing step of an online contract, authorization, or consent form
- Proof-of-delivery screens in delivery and logistics apps
- Membership or check-in forms for clinics, gyms, and rental services
- On-site sign-in and acceptance sheets on a tablet
- Not a good fit: flows where the user only needs to agree and you don't need to keep an image of the signature; a regular checkbox is enough
Installation
npx webberui@latest add https://webberui.com/r/signature-pad.en.jsonOr, once registries are configured in components.json, install it as @webberui/signature-pad.
Usage
import { SignaturePad } from "@/components/ui/signature-pad";
<SignaturePad
height={200}
onChange={(dataUrl) => {
// dataUrl is a PNG dataURL, or null when the canvas is empty
console.log(dataUrl);
}}
/>Examples
Store it in form state and check on submit
onChange passes null when the canvas is empty, so you can use it directly to tell whether the user has signed and to gate the submit button.
"use client";
import * as React from "react";
import { SignaturePad } from "@/components/ui/signature-pad";
export function ConsentForm() {
const [signature, setSignature] = React.useState<string | null>(null);
return (
<form
onSubmit={(e) => {
e.preventDefault();
// signature is a PNG dataURL
}}
>
<SignaturePad height={180} onChange={setSignature} />
<button type="submit" disabled={!signature}>
Submit consent form
</button>
</form>
);
}Upload to an API
Turn the dataURL into a Blob with fetch, then send it to your backend with FormData.
async function uploadSignature(dataUrl: string) {
const blob = await fetch(dataUrl).then((res) => res.blob());
const body = new FormData();
body.append("signature", blob, "signature.png");
await fetch("/api/signatures", { method: "POST", body });
}
<SignaturePad
onChange={(dataUrl) => {
if (dataUrl) void uploadSignature(dataUrl);
}}
/>Transparent background and custom ink
When the signature will be layered onto a PDF or another document, export a transparent PNG with exportBackground="transparent" and set blue ink with strokeColor.
<SignaturePad
strokeColor="#1d4ed8"
minWidth={1}
maxWidth={3}
exportBackground="transparent"
fileName="contract-signature"
placeholder="Sign here to accept"
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
strokeColor | string | "currentColor" | Ink colour; by default it reads the container's text colour at the moment the stroke starts, adapting to light and dark themes automatically |
minWidth | number | 1.5 | Thinnest line width during a fast stroke (px) |
maxWidth | number | 4 | Thickest line width during a slow stroke (px) |
height | number | 220 | Height of the signature area (px); the width fills the container automatically |
placeholder | string | "Sign here" | Watermark text shown before any stroke is drawn |
fileName | string | "signature" | File name for the PNG download (without the extension) |
exportBackground | string | "#ffffff" | Background colour of the exported PNG; set "transparent" to output a transparent background |
onChange | (dataUrl: string | null) => void | — | Callback after every stroke ends, undo, or clear; passes null when the canvas is empty |
className | string | — | Forwarded to the outermost container |
Accessibility
- The signature area carries
role="img"andaria-label="Handwritten Signature Area", and the toolbar buttons are nativebuttons with text labels and adisabledstate - It uses Pointer Events with
touch-action: none, so mouse, finger, and stylus all write; a stylus uses its real pressure values - When the user has "reduce motion" enabled at the system level, the watermark transition and the button micro-scale animation are disabled, while the writing functionality is completely unaffected
- A handwritten signature inherently requires a pointing device; in a production form, please also offer an alternative way to consent (for example a checkbox plus a typed name)
FAQ
Does it work with the Next.js App Router?
Yes. The file already starts with "use client", so you can import it from a Server Component page as usual.
How do I get the signature image?
After every finished stroke, undo, or clear, onChange receives a PNG dataURL, or null when the canvas is empty. Users can also press "Download PNG" in the toolbar; the file name comes from fileName.
Is the exported image sharp enough?
The canvas sizes its pixel buffer by devicePixelRatio, and when the container resizes it reallocates the buffer and redraws every stroke, so exported PNGs stay sharp on high-density screens.
Is stylus pressure supported?
Yes. When pointerType is pen and a pressure value is present, the line width follows the real pressure between minWidth and maxWidth. Mouse and finger input use writing speed instead: the faster the stroke, the thinner the line.
What are the dependencies?
motion, lucide-react, clsx, and tailwind-merge. If you'd rather skip the CLI, the Manual tab in the installation section walks you through copying the source.