@mshafiqyajid/react-signature
Headless signature hook and styled pad. Pointer-events API for unified desktop and mobile drawing, Bézier curve smoothing, velocity-sensitive stroke width, undo history, and an imperative ref API. Export as PNG, JPEG, or SVG. Zero dependencies.
Playground #
import { SignatureStyled } from "@mshafiqyajid/react-signature/styled";
import "@mshafiqyajid/react-signature/styles.css";
<SignatureStyled />Install #
npm install @mshafiqyajid/react-signature Quick start #
import { SignatureStyled } from "@mshafiqyajid/react-signature/styled";
import "@mshafiqyajid/react-signature/styles.css";
<SignatureStyled
penColor="#18181b"
penWidth={2}
onEnd={(dataURL) => console.log(dataURL)}
/> Pen customization #
Control the ink color, base width, and how much drawing speed tapers the stroke. velocitySensitivity maps pen speed to width between minWidth and maxWidth — set it to 0 for uniform strokes.
{/* Fountain-pen feel: fast strokes go thin, slow strokes go thick */}
<SignatureStyled
penColor="#1e3a5f"
penWidth={3}
velocitySensitivity={0.85}
minWidth={1}
maxWidth={6}
/>
{/* Uniform marker — no speed taper */}
<SignatureStyled
penColor="#dc2626"
penWidth={4}
velocitySensitivity={0}
/>
{/* Disable Bézier smoothing for a raw/pixelated look */}
<SignatureStyled penColor="#18181b" smoothing={false} /> Imperative ref API #
Forward a ref to access clear(), undo(), getDataURL(), and isEmpty() imperatively from parent components.
import { useRef } from "react";
import { SignatureStyled } from "@mshafiqyajid/react-signature/styled";
import type { SignatureHandle } from "@mshafiqyajid/react-signature/styled";
import "@mshafiqyajid/react-signature/styles.css";
function SignaturePad() {
const ref = useRef<SignatureHandle>(null);
return (
<>
<SignatureStyled ref={ref} height={200} />
<button onClick={() => ref.current?.clear()}>Clear</button>
<button onClick={() => ref.current?.undo()}>Undo last stroke</button>
<button onClick={() => {
if (!ref.current?.isEmpty()) {
const url = ref.current?.getDataURL("image/png");
// upload or display url
}
}}>
Save
</button>
</>
);
} Export / save #
getDataURL() accepts "image/png" (default), "image/jpeg", or "image/svg+xml". The return value is a data URL you can open, download, or POST to a server.
// PNG (default)
const png = ref.current?.getDataURL();
// JPEG (lossy, smaller file)
const jpeg = ref.current?.getDataURL("image/jpeg");
// SVG (vector, scalable)
const svg = ref.current?.getDataURL("image/svg+xml");
// Open in a new tab
window.open(ref.current?.getDataURL());
// Trigger a download
const a = document.createElement("a");
a.href = ref.current?.getDataURL() ?? "";
a.download = "signature.png";
a.click(); Erase mode #
Set mode="erase" to switch the pointer into an eraser. Erase strokes use destination-out compositing to cut through existing ink. The data-mode attribute on the root element reflects the current mode so you can style the cursor or UI accordingly.
<SignatureStyled mode="erase" height={200} />
{/* Toggle between draw and erase from parent state */}
const [mode, setMode] = useState<"draw" | "erase">("draw");
<SignatureStyled mode={mode} height={200} />
<button onClick={() => setMode(m => m === "draw" ? "erase" : "draw")}>
{mode === "draw" ? "Switch to Erase" : "Switch to Draw"}
</button> Built-in toolbar #
Add showToolbar to render a built-in control row beneath the canvas. The toolbar includes Draw/Erase mode buttons, a color picker, a width slider, an Undo button, and a Clear button. All controls are managed internally — no extra state needed.
<SignatureStyled showToolbar height={200} />
{/* Toolbar respects disabled state */}
<SignatureStyled showToolbar disabled height={200} /> Default value #
Pass a data URL to defaultValue to pre-load an existing signature image onto the canvas on mount. This sets isEmpty to false immediately. It is uncontrolled — the value is only read once on mount.
{/* Load a previously saved signature */}
const savedDataURL = localStorage.getItem("my-signature") ?? undefined;
<SignatureStyled
defaultValue={savedDataURL}
onEnd={(url) => localStorage.setItem("my-signature", url)}
height={200}
/> Headless #
Use the useSignature hook to bring your own canvas markup and controls while keeping the drawing logic.
import { useSignature } from "@mshafiqyajid/react-signature";
function MyPad() {
const { canvasRef, canvasProps, isEmpty, clear, getDataURL, undo } =
useSignature({
penColor: "#18181b",
penWidth: 2,
smoothing: true,
onEnd: (dataURL) => console.log(dataURL),
});
return (
<div className="my-pad">
<canvas
ref={canvasRef}
{...canvasProps}
width={600}
height={200}
className="my-canvas"
/>
<div className="my-toolbar">
<button onClick={clear} disabled={isEmpty}>Clear</button>
<button onClick={undo} disabled={isEmpty}>Undo</button>
<button onClick={() => console.log(getDataURL())} disabled={isEmpty}>
Save
</button>
</div>
</div>
);
} API #
| Prop | Type | Default | Description |
|---|---|---|---|
| penColor | string | "#18181b" | Ink color (any valid CSS color string) |
| penWidth | number | 2 | Base stroke width in px (used as center when velocity sensitivity > 0) |
| backgroundColor | string | "transparent" | Canvas fill color. Use a solid color when exporting as JPEG |
| smoothing | boolean | true | Bézier curve smoothing. Disable for a raw, pixelated look |
| velocitySensitivity | number | 0.7 | How much pen speed tapers stroke width. 0 = uniform, 1 = maximum taper |
| minWidth | number | 1 | Minimum stroke width (px) when velocity sensitivity > 0 |
| maxWidth | number | 4 | Maximum stroke width (px) when velocity sensitivity > 0 |
| onBegin | () => void | — | Called when a new stroke starts |
| onEnd | (dataURL: string) => void | — | Called when a stroke ends, receives the current canvas data URL |
| onChange | (isEmpty: boolean) => void | — | Called when the empty state changes |
| mode | "draw" | "erase" | "draw" | Drawing mode. Erase mode removes ink using destination-out compositing |
| defaultValue | string | — | Data URL to pre-load onto the canvas on mount (uncontrolled) |
| showToolbar | boolean | false | Show a built-in toolbar with mode toggle, color picker, width slider, undo and clear |
| disabled | boolean | false | Disable all drawing interaction |
| width | number | string | "100%" | Container width |
| height | number | 200 | Canvas height in px |
| className | string | — | Extra class on root wrapper |
| style | CSSProperties | — | Inline style on root wrapper |
Imperative ref methods #
| Method | Signature | Description |
|---|---|---|
| clear | () => void | Clears all strokes and resets isEmpty to true |
| undo | () => void | Removes the last drawn stroke and redraws |
| getDataURL | (type?: "image/png" | "image/jpeg" | "image/svg+xml") => string | Exports current canvas as a data URL |
| isEmpty | () => boolean | Returns true when no strokes have been drawn (or after clear) |
Data attributes #
| Attribute | Element | When present |
|---|---|---|
| data-disabled | .rsig-root | disabled prop is true |
| data-empty | .rsig-root | No strokes drawn (or after clear) |
| data-mode | .rsig-root | Current mode: "draw" or "erase" |
CSS variables #
:root {
--rsig-border-color: #d4d4d8;
--rsig-bg: #ffffff;
--rsig-radius: 8px;
--rsig-toolbar-bg: #f4f4f5;
--rsig-toolbar-border: #e4e4e7;
--rsig-btn-color: #3f3f46;
--rsig-btn-bg-hover: #e4e4e7;
--rsig-btn-bg-active: #d4d4d8;
--rsig-btn-radius: 6px;
--rsig-btn-font-size: 0.8125rem;
--rsig-focus-ring: 0 0 0 2px #3b82f6;
}