react ~4 KB 0 deps v0.1.0 ↗ GitHub ↗

@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 #

Props
TSX
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 #

PropTypeDefaultDescription
penColorstring"#18181b"Ink color (any valid CSS color string)
penWidthnumber2Base stroke width in px (used as center when velocity sensitivity > 0)
backgroundColorstring"transparent"Canvas fill color. Use a solid color when exporting as JPEG
smoothingbooleantrueBézier curve smoothing. Disable for a raw, pixelated look
velocitySensitivitynumber0.7How much pen speed tapers stroke width. 0 = uniform, 1 = maximum taper
minWidthnumber1Minimum stroke width (px) when velocity sensitivity > 0
maxWidthnumber4Maximum stroke width (px) when velocity sensitivity > 0
onBegin() => voidCalled when a new stroke starts
onEnd(dataURL: string) => voidCalled when a stroke ends, receives the current canvas data URL
onChange(isEmpty: boolean) => voidCalled when the empty state changes
mode"draw" | "erase""draw"Drawing mode. Erase mode removes ink using destination-out compositing
defaultValuestringData URL to pre-load onto the canvas on mount (uncontrolled)
showToolbarbooleanfalseShow a built-in toolbar with mode toggle, color picker, width slider, undo and clear
disabledbooleanfalseDisable all drawing interaction
widthnumber | string"100%"Container width
heightnumber200Canvas height in px
classNamestringExtra class on root wrapper
styleCSSPropertiesInline style on root wrapper

Imperative ref methods #

MethodSignatureDescription
clear() => voidClears all strokes and resets isEmpty to true
undo() => voidRemoves the last drawn stroke and redraws
getDataURL(type?: "image/png" | "image/jpeg" | "image/svg+xml") => stringExports current canvas as a data URL
isEmpty() => booleanReturns true when no strokes have been drawn (or after clear)

Data attributes #

AttributeElementWhen present
data-disabled.rsig-rootdisabled prop is true
data-empty.rsig-rootNo strokes drawn (or after clear)
data-mode.rsig-rootCurrent 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;
}
Edit this page on GitHub