Skip to content

Signature Pad

Draw or type a signature on one surface, accept it to lock it, and export it as SVG or PNG.

import { SignaturePad, SignaturePadHandle } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a Signature Pad where a person consents or confirms by signing: agreements, deliveries, check-ins. Take the signature in onAccept: the button waits for it, and the pad locks once it resolves.

import { useRef, useState } from 'react'
import { Field, FieldLabel, SignaturePad, type SignaturePadHandle, type SignatureValue } from '@youj/react'

export function Agreement() {
  const pad = useRef<SignaturePadHandle>(null)
  const [signature, setSignature] = useState<SignatureValue | null>(null)
  return (
    <Field>
      <FieldLabel>Signature</FieldLabel>
      <SignaturePad
        ref={pad}
        value={signature}
        onValueChange={setSignature}
        onAccept={() => upload(pad.current!.toSVG())}
      />
    </Field>
  )
}

The value is { kind: 'drawn', strokes } or { kind: 'typed', text }, and null while the pad is empty. A stroke is a list of [x, y, pressure] points, x and y as fractions of the pad's width, so the pad can resize (it keeps aspectRatio, 3 by default) and the signature scales with it. The ref's toSVG() returns a standalone SVG document and toDataURL(scale) a PNG on a transparent ground, both in the ink color of the moment; clear() and undo() work the pad. locked, defaultLocked and onLockedChange hold the lock from outside, say to show a signature on file already accepted.

Reference

Propsvalue, onAccept, locked, typeInstead, aspectRatio, name, required, readOnly…

Props

SignaturePad props
PropType
valueSignatureValue | null–
defaultValueSignatureValue | null

default null

null
onValueChange(value: SignatureValue | null) => void–
onAccept(value: SignatureValue) => Promise<unknown> | unknown

Runs when the signer presses Accept, with the signature. The button waits for it (spinner), then shows its check and the pad locks; a rejection shows its message on the button and keeps the pad open. Without it, Accept locks at once.

–
lockedboolean

The signature is accepted and read-only (controlled).

–
defaultLockedboolean

Starts accepted and read-only (uncontrolled).

default false

false
onLockedChange(locked: boolean) => void

Called with true once a signature is accepted, and false when Re-sign unlocks it.

–
typeInsteadboolean

Offers Sign by typing beside Draw, so a signature needs no pointer.

default true

true
aspectRationumber

The signing area's width over its height.

default 3

3
strokeWidthnumber

The pen at full pressure, in pixels on a 500px-wide pad; it scales with the pad.

default 3

3
typeFontstring

The face a typed signature is set in.

default SCRIPT_FONT

SCRIPT_FONT
namestring

Submits the accepted signature as a PNG data URL with a native form.

–
requiredboolean

Blocks a native submit until a signature is accepted.

–
disabledboolean–
readOnlyboolean

Shows the signature without tabs, Undo, Clear or Accept.

default false

false
labelstring

The pad's accessible name when no Field label names it. Default: the labels' "Signature".

–
soundboolean

With sound on (YoujProvider’s sound), the tabs and the Accept outcome sound. false keeps this pad silent.

default true

true
refRef<SignaturePadHandle>

The handle: clear, undo, toSVG, toDataURL.

–
Other props pass through: extends ComponentProps<'div'>.

Data attributes

The root carries data-yj-mode (draw or type), and data-yj-empty, data-yj-locked, data-disabled and data-readonly when they hold.

MotionTabs · accept · tick · settle · reduced motion

Motion

Motion tokens
MomentTokens
Tabsthe indicator glides between Draw and Sign by typing; the new area fades in (the Tabs motion)
spring.smooth

0.36s, bounce 0

0.36s, bounce 0
Sign herethe placeholder fades out as the first ink arrives, and back when the pad is cleared
duration.fastease.standard

140ms · cubic-bezier(0.2, 0, 0, 1)

140mscubic-bezier(0.2, 0, 0, 1)
Inkfollows the pointer sample by sample, never eased in time
no token

Acceptthe AsyncButton lifecycle: spinner, then a drawn check (or a shake and the error), then Re-sign
spring.snappy

0.2s, bounce 0

0.2s, bounce 0
Acceptedthe signing line eases to the success ink and a small tick draws at its end
duration.slowease.standard

340ms · cubic-bezier(0.2, 0, 0, 1)

340mscubic-bezier(0.2, 0, 0, 1)
Settlethe locked ink eases a touch smaller onto its line
duration.slowease.emphasized

340ms · cubic-bezier(0.6, 0, 0.1, 1)

340mscubic-bezier(0.6, 0, 0.1, 1)

Reduced motion

The tick, the line color, the settle, the tab indicator and the placeholder swap at once. Ink is drawn the same.

Reduced motion

Each state shows at once, so the pad reads the same. With sound on, choosing a tab and the Accept outcome sound as the rest of the kit does (sound={false} silences the pad).

AccessibilityKeyboard, ARIA, typing instead

Accessibility

  • Draw and Sign by typing are tabs: arrow keys move between them, Enter or Space chooses one. Choosing Sign by typing puts the caret in its field.
  • The drawing area is a focusable group named by its Field label (or label) and described by a hint: draw with a pointer, or press Enter to type the name instead. ⌘Z or Ctrl+Z on it undoes a stroke.
  • Sign by typing is a text field, so a signature never needs a pointer: type, then Tab to Accept.
  • Undo and Clear are named buttons with tooltips; a clear is announced in a polite live region.
  • Accept announces its progress and outcome (an error assertively); once it settles, the lock is announced politely, and the locked pad reads "Signed" with the time. Re-sign's unlock is announced too.
  • With required, a pad without an accepted signature stops a native submit and moves focus to the pad (or to Accept when there is a draft); the Field's error explains why.