# React Signature Pad: Smooth Ink, Typed Mode, PNG and SVG

> Build a React signature pad that survives phone rotation, keeps the page scrolling, offers a typed mode for keyboard users and exports clean PNG or SVG.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 16, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 9 min
- Canonical: https://wingo-ui.com/blog/react-signature-pad

## TL;DR

A good React signature pad draws on a canvas sized to the device pixel ratio, sets touch-action: none on that canvas only so the rest of the page still scrolls, and keeps the strokes as data so a resize or a phone rotation never wipes them. It also needs a typed-name mode for keyboard and screen reader users, and exports that ignore the theme: dark ink on a transparent PNG or SVG, cropped to the signature. The Wingo UI Signature Pad does all of that on top of signature_pad 5.1.4, with one controlled value you can store as JSON.

A React signature pad looks like the easiest component in a form: a canvas, a few pointer listeners, a Clear button. Then real people use it. The page refuses to scroll on a phone, the signature vanishes when someone rotates the screen, the ink is invisible in dark mode, and the PNG you saved has a black background. This guide covers what a react signature pad component has to get right, the props that matter on the Wingo UI [Signature Pad](https://wingo-ui.com/components/signature-pad), and how to drop it into a multi-step form.

## What does a React signature pad need to get right?

Six things: smooth ink, a sharp canvas, a page that still scrolls, strokes that survive resizes, a second way to sign and exports that ignore the theme.

1. **Smooth ink.** Raw pointer points joined with straight lines look like a seismograph. Bezier curves with a width that changes along the stroke look like a pen.
2. **A sharp canvas.** The canvas buffer has to be its CSS size times `devicePixelRatio`, or the ink is blurry on every phone.
3. **Scrolling that still works.** The canvas needs `touch-action: none` so a finger draws instead of panning. Everything else on the page must keep scrolling.
4. **Strokes that survive resizes.** Resizing a canvas clears it. If you only keep pixels, a rotation erases the signature.
5. **Another way to sign.** Drawing needs a pointer. Keyboard and screen reader users need a typed path.
6. **Exports that ignore the theme.** White ink in dark mode is right on screen and wrong on a contract.

Here is where the common options stand, as of October 2026:

| Package | Version | License | What you get |
| --- | --- | --- | --- |
| [signature_pad](https://www.npmjs.com/package/signature_pad) | 5.1.4 | MIT | the ink engine: curves, speed-based width, `toData` / `fromData`, `toDataURL`, `toSVG`; no React, no UI |
| [react-signature-canvas](https://www.npmjs.com/package/react-signature-canvas) | 1.1.0-alpha.2 | Apache-2.0 | a thin React wrapper over signature_pad with a ref API and `getTrimmedCanvas()`; [its README](https://github.com/agilgur5/react-signature-canvas) sets `clearOnResize` to true by default |
| [react-signature-pad](https://www.npmjs.com/package/react-signature-pad) | 0.0.6 | MIT | an old wrapper, last published in 2016, with `isEmpty`, `clear`, `toDataURL` and a stylesheet to include |
| [Wingo UI Signature Pad](https://wingo-ui.com/components/signature-pad) | registry item | Pro | signature_pad plus the field anatomy, Draw / Type modes, undo, theme-aware ink, resize-safe strokes, trimmed PNG / SVG export, a hidden form input |

If you only need a canvas inside an internal tool, react-signature-canvas is fine and costs nothing. Its ink is the same signature_pad engine. What it leaves to you is the rest of the list: strokes that survive a resize, a typed mode, and exports that do not depend on the theme.

## Why does signature ink thin and swell without pen pressure?

Because the width comes from speed. In signature_pad 5.1.4 every point records the pointer's `pressure`, but the width of each curve segment is `maxWidth / (velocity + 1)`, clamped at `minWidth`. The velocity is a running average: `velocityFilterWeight` times the speed of the newest segment plus the rest of the previous velocity. Slow down at the end of a letter and the line swells; flick across the baseline and it thins. That reads like a pen on paper on every device, including a mouse and a finger that report no pressure at all.

The Wingo UI pad exposes those three numbers as props, with slightly heavier defaults than the library (0.8 and 2.8 px instead of 0.5 and 2.5) because thin ink disappears on a phone held at arm's length:

```tsx
<SignaturePad
  label="Recipient signature"
  minWidth={1}
  maxWidth={4}
  velocityFilterWeight={0.5}
/>
```

A lower `velocityFilterWeight` gives calmer ink, because each new segment counts for less in the average. 1 lets every change in speed show, and 0 freezes the width at `maxWidth`. The default is 0.7, the same as the library. A tap leaves a dot.

> Live demo (Signature Pad): Sign the recipient pad with a finger or the mouse. Move slowly, then fast, and watch the line swell and thin; press Undo, then Download PNG or SVG. Try it at [Signature Pad](https://wingo-ui.com/components/signature-pad) and install it with `npx wingo-ui@latest add signature-pad`.

## How do I keep the page scrolling on a phone?

Put `touch-action: none` on the canvas and nowhere else. signature_pad sets it on the canvas when you call `on()` and restores `auto` on `off()`, and the Wingo UI pad only turns the pen on while it is interactive and in Draw mode. A swipe that starts outside the dashed area scrolls the page as usual; a swipe that starts inside draws. Disabled, read-only and Type mode give the canvas back to the page.

If people sign with a finger, `touch-action` is only half the fix. Two layout rules matter just as much:

- **Keep the pad short enough to scroll past.** The default `height` is 200px. A courier screen can go to 240px, but a pad that fills the viewport traps the thumb, because every swipe on it draws.
- **Keep it out of horizontal swipers.** Inside a carousel or swipeable tabs the two gestures fight. The [Form Wizard](https://wingo-ui.com/components/form-wizard) changes steps only through its buttons, never on swipe, so a pad inside a step keeps its gesture.

Our [Tailwind touch manipulation guide](https://wingo-ui.com/blog/tailwind-touch-manipulation) explains when `touch-none` is right and when it traps people.

## How do I keep the signature when the phone rotates?

Keep the strokes as data and repaint them; never rely on the canvas pixels. The pad keeps every stroke as points in CSS pixels of the box it was drawn in. A `ResizeObserver` on the canvas resets the buffer to the new size times `devicePixelRatio` and repaints the strokes, scaled uniformly to fit. Rotate the phone, open the keyboard, drag a desktop window narrower: the signature stays and keeps its shape.

The same data is the value. `onValueChange` hands you a `SignatureData` object, plain JSON you can store and load back. For signature pad TypeScript code, the exported type is a union on `kind`:

```ts
type SignatureData =
  | { kind: "drawn"; strokes: SignatureStroke[]; width: number; height: number }
  | { kind: "typed"; text: string; font: string }
```

`width` and `height` are the box the strokes were drawn in. The repaint scale is the smaller of the width and height ratios, so a signature shrinks to fit a narrower pad and keeps its size in a wider one: one captured on a 360px phone fits a 600px desktop pad later. `onValueChange` fires when a stroke ends, never on every point, so a controlled pad does not re-render 60 times a second.

## How do people sign without drawing?

With a typed name. [WCAG 2.1.1 Keyboard](https://www.w3.org/WAI/WCAG21/Understanding/keyboard.html) exempts input that depends on the path of the pointer, and the W3C's explainer names freehand drawing as an example. Whether a signature counts is a judgment call: the same page says handwriting does not, because its underlying function is entering text. Either way, a drawing-only pad leaves keyboard and screen reader users unable to finish your form. Type mode is their path.

The Draw / Type switch is a [Segmented Control](https://wingo-ui.com/components/segmented-control) at the top, the first stop on Tab. In Type mode the name appears on the baseline in a system script font (the `DEFAULT_SIGNATURE_FONT` stack: Segoe Script, Snell Roundhand, Brush Script MT, Apple Chancery, cursive) and shrinks to fit when it is long. Nothing downloads, and exports use the same stack. Each mode keeps its own draft, so switching back restores the strokes or the name.

The drawing area itself is focusable and named by the label. With focus on it, mod+Z undoes the last stroke and Delete or Backspace clears it. A live region announces "Signature added" and "Signature cleared".

> Live demo (Signature Pad): Switch to Type and enter a name: it lands on the baseline in a script font. Switch back to Draw and your strokes are still there. Try it at [Signature Pad](https://wingo-ui.com/components/signature-pad) and install it with `npx wingo-ui@latest add signature-pad`.

Use `modes={["draw"]}` only when another signing path exists, such as a courier app where the driver hands the phone over and a support line handles everyone else.

## How do I export the signature as PNG or SVG?

Through the api that `setApi` hands you. Every export uses `exportColor` (default `#111111`) on a transparent background, whatever the theme, and crops to the signature plus 12px when `trimExport` is on:

```tsx
"use client"

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { SignaturePad, isSignatureEmpty, type SignatureData, type SignaturePadApi } from "@/components/ui/signature-pad"

export function SignContract() {
  const [signature, setSignature] = useState<SignatureData | null>(null)
  const [api, setApi] = useState<SignaturePadApi | null>(null)

  const save = async () => {
    if (!api || isSignatureEmpty(signature)) return
    const png = await api.toBlob({ scale: 2 })
    const body = new FormData()
    body.append("image", png, "signature.png")
    body.append("strokes", JSON.stringify(signature))
    await fetch("/api/contracts/118/signature", { method: "POST", body })
  }

  return (
    <div className="flex max-w-md flex-col gap-4">
      <SignaturePad
        label="Client signature"
        description="Sign with your finger or mouse"
        required
        value={signature}
        onValueChange={setSignature}
        setApi={setApi}
        exportColor="#1F3A93"
        fileName="Client signature"
      />
      <div className="flex gap-2">
        <Button disabled={isSignatureEmpty(signature)} onClick={save}>
          Sign contract
        </Button>
        <Button variant="soft" disabled={isSignatureEmpty(signature)} onClick={() => api?.download({ format: "svg" })}>
          Download SVG
        </Button>
      </div>
    </div>
  )
}
```

- `toBlob(options)` resolves with a PNG by default. `type`, `quality`, `trim`, `scale` (device pixels per CSS pixel, default 2), `padding`, `background` and `color` override the defaults per call.
- `toSVG()` returns an SVG string, good for a PDF generator or an email that should stay sharp at any zoom.
- `toDataURL()` is synchronous, handy for an `<img src>` preview.
- `download({ format })` saves the file as `fileName` plus the local date and time and resolves with the name.

Send both the image and the stroke JSON. The PNG goes on the document; the JSON lets you redraw the signature at any size later. The download and blob helpers come from [File Export](https://wingo-ui.com/components/file-export), which the CLI installs with the pad.

For plain HTML forms, set `name="signature"` and a hidden input carries the trimmed PNG as a data URL, updated after each stroke. A form reset puts `defaultValue` back.

## How do I add a signature step to a multi-step form?

Give the step a `validate` that checks the value, and show the error on the pad. The pad is a regular field, so it slots into a [Form Wizard](https://wingo-ui.com/components/form-wizard) step like any input:

```tsx
"use client"

import { useState } from "react"
import { FormWizard, type WizardStep } from "@/components/ui/form-wizard"
import { Input } from "@/components/ui/input"
import { SignaturePad, isSignatureEmpty, type SignatureData } from "@/components/ui/signature-pad"

export function RentalAgreement() {
  const [name, setName] = useState("")
  const [signature, setSignature] = useState<SignatureData | null>(null)
  const [missing, setMissing] = useState(false)

  const steps: WizardStep[] = [
    {
      id: "renter",
      title: "Renter",
      content: <Input label="Full name" value={name} onValueChange={setName} autoComplete="name" />,
      validate: () => name.trim().length > 1,
    },
    {
      id: "sign",
      title: "Sign",
      content: (
        <SignaturePad
          label="Renter signature"
          description="Sign with your finger, or switch to Type"
          required
          value={signature}
          onValueChange={(next) => {
            setSignature(next)
            setMissing(false)
          }}
          error={missing ? "A signature is required" : undefined}
        />
      ),
      validate: () => {
        const ok = !isSignatureEmpty(signature)
        setMissing(!ok)
        return ok
      },
    },
  ]

  return (
    <div className="h-dvh md:h-auto">
      <FormWizard
        steps={steps}
        fullScreenOnMobile
        labels={{ submit: "Sign agreement" }}
        onComplete={() => fetch("/api/rentals", { method: "POST", body: JSON.stringify({ name, signature }) })}
      />
    </div>
  )
}
```

Keep the signature in parent state, as here. Steps that are not active unmount, so a value that lives only inside the step would be gone by the time `onComplete` runs. `isSignatureEmpty` is true for `null`, a drawn value with no strokes and a typed value with a blank name, so one check covers both modes. Below 768px, `fullScreenOnMobile` (on by default, written out here for clarity) fills the container's height with a sticky header, a scrolling body and a sticky footer of 48px buttons above the safe area. It needs a container with a height, which is why the wizard sits in an `h-dvh` wrapper that lets go from 768px up. Our [React multi step form tutorial](https://wingo-ui.com/blog/react-multi-step-form) covers zod schemas per step and saved drafts.

## What are the common React signature capture pitfalls?

- **Storing only the PNG.** You cannot redraw it larger, recolor it or replay the stroke order later (every point carries a timestamp in the JSON). Store the JSON too.
- **Exporting the screen ink.** `penColor` defaults to `currentColor`, so the ink is white in dark mode and repaints when the theme changes. Exports use `exportColor` so a dark-mode signature does not vanish on a white page.
- **A pad as tall as the screen.** Every swipe on it draws, so people cannot scroll past. The 200px default fits a phone; 240px is the roomy end.
- **Treating the image as the whole e-signature.** A picture of a name is one piece of evidence. Record who signed, when and which version of the document, and ask a lawyer what your jurisdiction requires.
- **Hiding Type to save space.** It is the only path for keyboard users. Keep it unless your product has another one.

For the rest of the form layer (fields, errors, zod and mobile keyboards) read our pillar on [React form components](https://wingo-ui.com/blog/react-form-components-guide), or browse every [component guide](https://wingo-ui.com/blog/category/components).

## How do I install the Signature Pad?

`npx wingo-ui@latest add signature-pad` copies the component with the Field, Button, Input, Segmented Control, Tooltip and File Export pieces plus the state, motion and UI config helpers it imports, and installs `signature_pad`, `motion` and `lucide-react`. Signature Pad is part of [Wingo UI Pro](https://wingo-ui.com/pricing), so run `npx wingo-ui@latest login` first. Add the wizard with `npx wingo-ui@latest add form-wizard`.

## Components in this post

- [Signature Pad](https://wingo-ui.com/components/signature-pad) (Pro): A React signature pad: draw with a finger, pen or mouse in ink that thins with speed, or type a name, then export it as PNG or SVG. Install: `npx wingo-ui@latest add signature-pad`
- [Form Wizard](https://wingo-ui.com/components/form-wizard) (Pro): A React multi-step form wizard with a stepper, sync or async validation per step, a summary step and a full-screen phone layout. Install: `npx wingo-ui@latest add form-wizard`
- [File export](https://wingo-ui.com/components/file-export) (Pro): File export helpers for React apps: download blobs, text and JSON, turn a canvas or an SVG into a PNG, copy images and name files cleanly. Install: `npx wingo-ui@latest add file-export`

## FAQ

### What is the best React signature pad library?

Most React signature components wrap signature_pad (MIT, 5.1.4 as of October 2026), which does the smooth ink. react-signature-canvas is the common thin wrapper; the Wingo UI Signature Pad adds a typed mode, theme-aware ink, resize-safe strokes and PNG / SVG export as a copy-in component.

### Does a signature pad use real pen pressure?

Not in signature_pad 5.1.4. It records the pointer's pressure with each point, but the line width comes from speed: maxWidth divided by velocity plus one, never thinner than minWidth. Slow strokes swell and fast ones thin, which reads like a pen without needing a pressure-sensitive stylus.

### Why does my signature disappear when the phone rotates?

Resizing a canvas clears its pixels, and react-signature-canvas also clears on resize by default (clearOnResize is true). Keep the strokes as point data and repaint them, scaled to the new box, after every resize; the Wingo UI pad does this with a ResizeObserver.

### How do I save a React signature as an image?

Export a PNG blob for the document and keep the stroke JSON for redrawing later. With the Wingo UI pad, call api.toBlob() for a trimmed PNG, api.toSVG() for a vector string, or api.download({ format: "svg" }) to save a file.

### Can people sign with a keyboard?

Not by drawing, because freehand ink depends on the path of the pointer. WCAG 2.1.1 exempts path-dependent input from the keyboard requirement, but the safe choice is a second way to sign, such as a typed name in a script font, which is the Type mode of the Wingo UI pad.

### Is the Wingo UI Signature Pad free?

No. Signature Pad and Form Wizard are Pro components. The Field, Input and Segmented Control it builds on are free.

---

Source: https://wingo-ui.com/blog/react-signature-pad
