---
title: "Capture Signatures in React with React Signature Canvas"
slug: "capture-signatures-in-react-with-react-signature-canvas"
published: "2025-10-30"
updated: "2025-11-12"
validated: "2025-10-23"
categories:
  - "React"
tags:
  - "React signature canvas"
  - "form validation React"
  - "react signature capture"
  - "Zustand React"
  - "React Hook Form"
  - "signature persistence"
  - "Next.js signature form"
  - "legal signature capture"
llm-intent: "reference"
audience-level: "intermediate"
framework-versions:
  - "react"
  - "next.js"
  - "zustand"
  - "react-hook-form"
  - "react-signature-canvas"
status: "stable"
llm-purpose: "Discover how to capture signatures in React using react-signature-canvas, with validation and data persistence for a seamless user experience. Start now!"
llm-prereqs:
  - "Access to React"
  - "Access to Next.js"
  - "Access to Zustand"
  - "Access to react-hook-form"
  - "Access to react-signature-canvas"
llm-outputs:
  - "Completed outcome: Discover how to capture signatures in React using react-signature-canvas, with validation and data persistence for a seamless user experience. Start now!"
---

**Summary Triples**
- (react-signature-canvas, renders, a canvas-based signature pad accessible via a ref for operations like toDataURL(), fromDataURL(), clear(), and isEmpty())
- (React Hook Form + Zod, validates, the signature field by treating the signature image as a required string (base64); schema: signature: z.string().min(1))
- (Zustand store, persists, the signature base64 string across pages/components so users can revisit and restore the pad)
- (Signature capture flow, stores, pad.getTrimmedCanvas().toDataURL('image/png') into a hidden controlled form field before submit)
- (Form submission, sends, a JSON payload with signerName and signature (base64 PNG) to a server endpoint (e.g., POST /api/signature) and then resets form state)
- (Clearing and restoring, is done by, calling padRef.clear() to remove ink and padRef.fromDataURL(savedBase64) to redraw a saved signature)
- (Legal acceptability, improves with, collecting signer metadata (full name, timestamp) and storing a base64 PNG as evidence rather than only path/coordinates)

### {GOAL}
Discover how to capture signatures in React using react-signature-canvas, with validation and data persistence for a seamless user experience. Start now!

### {PREREQS}
- Access to React
- Access to Next.js
- Access to Zustand
- Access to react-hook-form
- Access to react-signature-canvas

### {STEPS}
1. Install Dependencies and Scaffold the Form
2. Persist Form Data with Zustand
3. Build a Reusable Signature Pad
4. Connect React Hook Form with the Store and Pad
5. Accept the Signature on the Server
6. Launch the Signature Collection Page

<!-- llm:goal="Discover how to capture signatures in React using react-signature-canvas, with validation and data persistence for a seamless user experience. Start now!" -->
<!-- llm:prereq="Access to React" -->
<!-- llm:prereq="Access to Next.js" -->
<!-- llm:prereq="Access to Zustand" -->
<!-- llm:prereq="Access to react-hook-form" -->
<!-- llm:prereq="Access to react-signature-canvas" -->
<!-- llm:output="Completed outcome: Discover how to capture signatures in React using react-signature-canvas, with validation and data persistence for a seamless user experience. Start now!" -->

# Capture Signatures in React with React Signature Canvas
> Discover how to capture signatures in React using react-signature-canvas, with validation and data persistence for a seamless user experience. Start now!
Matija Žiberna · 2025-10-30

I was preparing a lightweight proof of concept that needed a legally acceptable signature field inside a plain React form. The usual canvas examples felt too detached from the realities of controlled forms, persistence, and server submission. After threading together `react-signature-canvas`, React Hook Form, and a Zustand store, I ended up with a repeatable pattern that you can plug into any project. This guide walks you through building that exact flow end to end.

## 1. Install Dependencies and Scaffold the Form

Start with any React or Next.js project. Install the packages that handle form state, drawing, and persistence. In a Next.js App Router project, run:

```
npm install react-hook-form zod @hookform/resolvers zustand react-signature-canvas
```

With those pieces available, create a basic form shell that will soon host the signature pad.

```tsx
// File: src/app/signature/page.tsx
'use client'

import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'

const schema = z.object({
  signerName: z.string().min(1, 'Please provide your full name'),
  signature: z.string().min(1, 'A signature is required')
})

type FormValues = z.infer<typeof schema>

export default function SignaturePage() {
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: {
      signerName: '',
      signature: ''
    }
  })

  const handleSubmit = form.handleSubmit(async (data) => {
    await fetch('/api/signature', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    form.reset()
  })

  return (
    <main className="mx-auto max-w-xl space-y-6 py-10">
      <h1 className="text-3xl font-semibold">Sign the consent form</h1>
      <form onSubmit={handleSubmit} className="space-y-4">
        <div className="space-y-2">
          <label className="block text-sm font-medium">Full name</label>
          <input
            {...form.register('signerName')}
            className="w-full rounded border px-3 py-2"
            placeholder="Jane Doe"
          />
          <p className="text-sm text-red-600">
            {form.formState.errors.signerName?.message}
          </p>
        </div>

        <p className="text-sm text-red-600">
          {form.formState.errors.signature?.message}
        </p>

        <button
          type="submit"
          className="rounded bg-blue-600 px-4 py-2 text-white font-medium disabled:opacity-60"
          disabled={form.formState.isSubmitting}
        >
          {form.formState.isSubmitting ? 'Submitting…' : 'Submit signature'}
        </button>
      </form>
    </main>
  )
}
```

This component provides a minimal page with form validation ready to plug in the signature field. The submit handler sends both the typed name and the Base64 signature to an API endpoint with JSON payloads. Next we will add a persistence layer so the signature survives route changes or reloads.

## 2. Persist Form Data with Zustand

Zustand keeps the captured signature accessible outside the form so you can preview or resend it without resketching. Create a small store that syncs the signer name and signature.

```ts
// File: src/lib/signature-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

type SignatureState = {
  signerName: string
  signature?: string
  setSignerName: (value: string) => void
  setSignature: (dataUrl?: string) => void
  reset: () => void
}

export const useSignatureStore = create<SignatureState>()(
  persist(
    (set) => ({
      signerName: '',
      signature: undefined,
      setSignerName: (value) => set({ signerName: value }),
      setSignature: (dataUrl) => set({ signature: dataUrl }),
      reset: () => set({ signerName: '', signature: undefined })
    }),
    { name: 'signature-demo' }
  )
)
```

The `persist` middleware writes the values to localStorage, so refreshing the page restores the signature preview immediately. Wiring this store into the form keeps the code organized while still allowing you to replace it with another persistence layer later if needed.

## 3. Build a Reusable Signature Pad

Now create the component that wraps `react-signature-canvas`, binds it to React Hook Form, and mirrors state into Zustand.

```tsx
// File: src/components/signature-pad.tsx
'use client'

import { useEffect, useRef } from 'react'
import SignatureCanvas from 'react-signature-canvas'
import { Controller, useFormContext } from 'react-hook-form'
import { useSignatureStore } from '@/lib/signature-store'

type SignaturePadProps = {
  name: 'signature'
}

export function SignaturePad({ name }: SignaturePadProps) {
  const canvasRef = useRef<SignatureCanvas>(null)
  const { control } = useFormContext()
  const signature = useSignatureStore((state) => state.signature)
  const setSignature = useSignatureStore((state) => state.setSignature)

  useEffect(() => {
    if (signature && canvasRef.current) {
      canvasRef.current.fromDataURL(signature)
    }
  }, [signature])

  return (
    <Controller
      control={control}
      name={name}
      render={({ field }) => (
        <div className="space-y-2">
          <label className="block text-sm font-medium">Signature</label>
          <div className="rounded border bg-white p-2">
            <SignatureCanvas
              ref={canvasRef}
              onEnd={() => {
                const dataUrl = canvasRef.current?.toDataURL()
                field.onChange(dataUrl)
                setSignature(dataUrl)
              }}
              canvasProps={{
                className: 'h-48 w-full cursor-crosshair rounded'
              }}
            />
          </div>
          <div className="flex justify-end gap-2">
            <button
              type="button"
              className="rounded border px-3 py-1 text-sm"
              onClick={() => {
                canvasRef.current?.clear()
                field.onChange(undefined)
                setSignature(undefined)
              }}
            >
              Clear
            </button>
            <button
              type="button"
              className="rounded border px-3 py-1 text-sm"
              onClick={() => {
                const dataUrl = canvasRef.current?.toDataURL()
                if (dataUrl) {
                  field.onChange(dataUrl)
                  setSignature(dataUrl)
                }
              }}
            >
              Save snapshot
            </button>
          </div>
        </div>
      )}
    />
  )
}
```

The component loads any stored signature when it mounts, captures strokes through the `onEnd` callback, and forwards the Base64 data to both the form and the store. The clear button resets every place where the signature lives, preventing stale previews or submissions. With this pad in place, the form page can consume it through React Hook Form’s context provider.

## 4. Connect React Hook Form with the Store and Pad

Update the page component to wrap the form with a `FormProvider`, bind the signer name field to Zustand, and insert the signature pad.

```tsx
// File: src/app/signature/page.tsx
'use client'

import { FormProvider, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { useSignatureStore } from '@/lib/signature-store'
import { SignaturePad } from '@/components/signature-pad'

const schema = z.object({
  signerName: z.string().min(1, 'Please provide your full name'),
  signature: z.string().min(1, 'A signature is required')
})

type FormValues = z.infer<typeof schema>

export default function SignaturePage() {
  const store = useSignatureStore()
  const form = useForm<FormValues>({
    resolver: zodResolver(schema),
    defaultValues: {
      signerName: store.signerName,
      signature: store.signature ?? ''
    }
  })

  const handleSubmit = form.handleSubmit(async (data) => {
    await fetch('/api/signature', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    })
    store.reset()
    form.reset()
  })

  return (
    <main className="mx-auto max-w-xl space-y-6 py-10">
      <h1 className="text-3xl font-semibold">Sign the consent form</h1>
      <FormProvider {...form}>
        <form onSubmit={handleSubmit} className="space-y-4">
          <div className="space-y-2">
            <label className="block text-sm font-medium">Full name</label>
            <input
              {...form.register('signerName', {
                onChange: (event) => store.setSignerName(event.target.value)
              })}
              className="w-full rounded border px-3 py-2"
              placeholder="Jane Doe"
            />
            <p className="text-sm text-red-600">
              {form.formState.errors.signerName?.message}
            </p>
          </div>

          <SignaturePad name="signature" />

          <p className="text-sm text-red-600">
            {form.formState.errors.signature?.message}
          </p>

          <button
            type="submit"
            className="rounded bg-blue-600 px-4 py-2 text-white font-medium disabled:opacity-60"
            disabled={form.formState.isSubmitting}
          >
            {form.formState.isSubmitting ? 'Submitting…' : 'Submit signature'}
          </button>
        </form>
      </FormProvider>
    </main>
  )
}
```

The store now mirrors every keystroke in the name field and every stroke on the canvas. Submissions send both values to the API and then wipe the persisted data so the pad returns to a clean state for the next signer. The only missing piece is the server-side endpoint that accepts and records the payload.

## 5. Accept the Signature on the Server

Implement a simple API route to receive the JSON payload. In a Next.js project, place the following handler under the App Router `app/api` directory.

```ts
// File: src/app/api/signature/route.ts
import { NextResponse } from 'next/server'
import { z } from 'zod'

const payload = z.object({
  signerName: z.string(),
  signature: z.string().startsWith('data:image')
})

export async function POST(request: Request) {
  const body = await request.json()
  const result = payload.safeParse(body)

  if (!result.success) {
    return NextResponse.json({ error: 'Invalid payload' }, { status: 400 })
  }

  const { signerName, signature } = result.data

  // Persist to your storage of choice here
  console.log('Captured signature from', signerName)
  console.log('Data URL length', signature.length)

  return NextResponse.json({ status: 'ok' })
}
```

This handler validates that the name exists and the signature field contains a data URL. Replace the `console.log` calls with database writes, S3 uploads, or any downstream process you need. Because the payload is already in Base64, it is ready to store or forward immediately.

## 6. Launch the Signature Collection Page

Wire the page into your routing so users can visit it directly. With Next.js this page already sits at `/signature`; in a Vite app you would render it inside your router of choice. Start the dev server, visit the page, draw a signature, and confirm that the server logs both the name and the data URL length. Clearing the pad should update both the visual canvas and the persisted state instantly.

## Conclusion

We started with a blank React page and ended with a fully functional signature capture flow that lets users sketch a signature, validates it alongside typed fields, preserves it with Zustand, and posts the Base64 data to an API endpoint. You now have a reusable blueprint for adding legally meaningful signatures to any client project. Let me know in the comments if you have questions, and subscribe for more practical development guides.

Thanks, Matija

## LLM Response Snippet
```json
{
  "goal": "Discover how to capture signatures in React using react-signature-canvas, with validation and data persistence for a seamless user experience. Start now!",
  "responses": [
    {
      "question": "How do I integrate react-signature-canvas with React Hook Form so the signature becomes a required field?",
      "answer": "Keep a ref to the SignaturePad component. On user actions (e.g., Save/Submit), call padRef.current?.getTrimmedCanvas().toDataURL('image/png') and set that base64 string into a hidden controlled field registered with react-hook-form (e.g., form.setValue('signature', dataUrl, { shouldValidate: true })). Validate via Zod schema: signature: z.string().min(1, 'A signature is required'). On submit, use form.handleSubmit to send form values."
    },
    {
      "question": "How can I persist a user's signature across routes or reloads using Zustand?",
      "answer": "Create a small Zustand store with state { signature: '' } and actions setSignature(url) and clearSignature(). When the pad is saved, setSignature(base64). On mount of the pad component, if store.signature exists call padRef.current?.fromDataURL(store.signature) and also set the form default value/form.setValue('signature', store.signature). Optionally persist Zustand to localStorage for reload persistence."
    },
    {
      "question": "What's the best way to submit the signature image to my server?",
      "answer": "Submit the base64 PNG as part of a JSON body: fetch('/api/signature', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signerName, signature: base64 }) }). On the server decode the base64 and save as a PNG blob or attach to your storage backend. Alternatively send as multipart/form-data by converting base64 to a Blob and appending to FormData."
    },
    {
      "question": "How do I clear the signature pad and update the form and store accordingly?",
      "answer": "Call padRef.current?.clear() to clear the canvas. Then update form and state: form.setValue('signature', '', { shouldValidate: true }); zustandStore.clearSignature(). If you use an isEmpty() check, use it to disable submit when pad is empty."
    },
    {
      "question": "How can I restore a saved signature into the pad when the user returns?",
      "answer": "When the pad component mounts, check your persistence (Zustand/localStorage/server). If you have a base64 string, call padRef.current?.fromDataURL(savedBase64) to redraw and call form.setValue('signature', savedBase64) so validation/state is consistent."
    },
    {
      "question": "How do I get a trimmed signature image (no whitespace) and ensure smaller payloads?",
      "answer": "Use getTrimmedCanvas() on the pad ref: const canvas = padRef.current?.getTrimmedCanvas(); const dataUrl = canvas.toDataURL('image/png'); That removes extra whitespace. Optionally reduce size with canvas.toDataURL('image/jpeg', 0.8) or resize the canvas before exporting."
    },
    {
      "question": "What server-side considerations make the captured signature legally binding?",
      "answer": "Persist signer metadata (full name, timestamp, IP address), store the exact PNG/base64 used for the signature, log the submission event and verification steps, and ensure tamper-evidence by storing hashes (SHA-256) of the image and metadata with secure timestamps. Consult legal counsel for jurisdiction-specific requirements."
    }
  ]
}
```