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/reactFor 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.
Description
An agreement's signature field: sign, then Accept files it, and the signature locks, its line turning green with a small tick.
The pad is one surface: Draw | Sign by typing tabs across the top, the signing area, and a footer with Undo and Clear and the Accept button. Each stroke is one filled path that swells and thins: a pen's own pressure, or for a mouse or a finger a pressure taken from speed (fast is thin). Accept is the kit's AsyncButton: it awaits onAccept(value) (spinner), draws its check, and the signature locks: read-only, its line in the success ink with a small tick at the end, its ink settled onto the line. A rejection shows its message on the button and leaves the pad open; Re-sign unlocks a locked signature.
A signature typed rather than drawn, set in a script face.
Sign by typing is the way to sign without a pointer: its tab, or Enter on the focused drawing area. Each mode keeps its own draft, so switching back brings the drawing back. The face is a stack of the script fonts systems ship, ending in cursive; pass typeFont to use one your app loads.
A native form with a required signature: submitting before the signature is accepted stops the form and shows the Field's error.
name submits the accepted signature as a PNG data URL with the form, and required (or a required Field) blocks submit until a signature is accepted: a draft doesn't count. In a Field the pad takes its label, description and invalid state.
Code
signature-pad-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsvalue, onAccept, locked, typeInstead, aspectRatio, name, required, readOnly…
Props
| Prop | Type | |
|---|---|---|
value | SignatureValue | null | – |
defaultValue | SignatureValue | nulldefault null | null |
onValueChange | (value: SignatureValue | null) => void | – |
onAccept | (value: SignatureValue) => Promise<unknown> | unknownRuns 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. | – |
locked | booleanThe signature is accepted and read-only (controlled). | – |
defaultLocked | booleanStarts accepted and read-only (uncontrolled). default false | false |
onLockedChange | (locked: boolean) => voidCalled with true once a signature is accepted, and false when Re-sign unlocks it. | – |
typeInstead | booleanOffers Sign by typing beside Draw, so a signature needs no pointer. default true | true |
aspectRatio | numberThe signing area's width over its height. default 3 | 3 |
strokeWidth | numberThe pen at full pressure, in pixels on a 500px-wide pad; it scales with the pad. default 3 | 3 |
typeFont | stringThe face a typed signature is set in. default SCRIPT_FONT | SCRIPT_FONT |
name | stringSubmits the accepted signature as a PNG data URL with a native form. | – |
required | booleanBlocks a native submit until a signature is accepted. | – |
disabled | boolean | – |
readOnly | booleanShows the signature without tabs, Undo, Clear or Accept. default false | false |
label | stringThe pad's accessible name when no Field label names it. Default: the labels' "Signature". | – |
sound | booleanWith sound on ( default true | true |
ref | Ref<SignaturePadHandle>The handle: | – |
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
| Moment | Tokens | ||
|---|---|---|---|
| Tabsthe indicator glides between Draw and Sign by typing; the new area fades in (the Tabs motion) | spring.smooth0.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.standard140ms · 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.snappy0.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.standard340ms · 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.emphasized340ms · 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.