Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Signature Pad

A surface the reader signs with a mouse, a finger or a pen.

Rendered by React 19.3.1

Loading React…
import { Field } from '@hamur/react/field'
import { SignaturePad } from '@hamur/react/signature-pad'

export default function SignaturePadExample() {
  return (
    <Field.Root className="field">
      <Field.Label>Signature</Field.Label>
      <SignaturePad.Root className="signature-pad">
        <SignaturePad.Control className="signature-pad-control">
          <SignaturePad.Guide className="signature-pad-guide">
            ×
          </SignaturePad.Guide>
          <SignaturePad.Strokes />
        </SignaturePad.Control>
        <SignaturePad.Undo className="button signature-pad-button">
          Undo
        </SignaturePad.Undo>
        <SignaturePad.Redo className="button signature-pad-button">
          Redo
        </SignaturePad.Redo>
        <SignaturePad.Clear className="button signature-pad-button">
          Clear
        </SignaturePad.Clear>
      </SignaturePad.Root>
      <Field.Description className="muted">
        Sign with your mouse, finger or a pen. Ctrl+Z undoes, Ctrl+Y redoes and Delete clears.
      </Field.Description>
    </Field.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<SignaturePad.Root>
  <SignaturePad.Control>
    <SignaturePad.Guide />
    <SignaturePad.Strokes />
  </SignaturePad.Control>
  <SignaturePad.Undo />
  <SignaturePad.Redo />
  <SignaturePad.Clear />
</SignaturePad.Root>

Examples

Form

A required pad in a Form with a validate that refuses a signature under 60 px wide; Send shows the PNG data URL, Reset empties it.

SignaturePadForm.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { SignaturePad, getSignatureBounds } from '@hamur/react/signature-pad'
import type { SignaturePadValue } from '@hamur/react/signature-pad'

function validate(value: unknown) {
  const bounds = getSignatureBounds(value as SignaturePadValue)
  return bounds && bounds.width < 60 ? 'Draw a signature at least 60 px wide.' : null
}

function sent(form: HTMLFormElement) {
  return [...new FormData(form)]
    .map(([key, value]) => {
      const text = String(value)
      return `${key}=${text.length > 40 ? `${text.slice(0, 40)}… (${text.length} characters)` : text}`
    })
    .join('&')
}

export default function SignaturePadFormExample() {
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) =>
        setSubmitted(sent(details.event.target as HTMLFormElement))
      }
      onReset={() => setSubmitted('')}
    >
      <Field.Root name="signature" className="field" validate={validate}>
        <Field.Label>Signature</Field.Label>
        <SignaturePad.Root className="signature-pad" required>
          <SignaturePad.Control className="signature-pad-control">
            <SignaturePad.Guide className="signature-pad-guide">×</SignaturePad.Guide>
            <SignaturePad.Strokes />
          </SignaturePad.Control>
          <SignaturePad.Undo className="button signature-pad-button">Undo</SignaturePad.Undo>
          <SignaturePad.Redo className="button signature-pad-button">Redo</SignaturePad.Redo>
          <SignaturePad.Clear className="button signature-pad-button">Clear</SignaturePad.Clear>
        </SignaturePad.Root>
        <Field.Description className="muted">
          The form sends the signature as a PNG data URL.
        </Field.Description>
        <Field.Error className="error" match="valueMissing">
          Sign before you send.
        </Field.Error>
        <Field.Error className="error" match="customError" />
      </Field.Root>
      <div className="row">
        <button className="button" type="reset">
          Reset
        </button>
        <button className="button" type="submit">
          Send
        </button>
      </div>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

Draw Or Type

Tabs to draw a signature or type a name in an Input; the pad is required only while the name is empty.

SignaturePadDrawOrType.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { SignaturePad } from '@hamur/react/signature-pad'
import { Tabs } from '@hamur/react/tabs'

function sent(form: HTMLFormElement) {
  return [...new FormData(form)]
    .map(([key, value]) => {
      const text = String(value)
      return `${key}=${text.length > 40 ? `${text.slice(0, 40)}… (${text.length} characters)` : text}`
    })
    .join('&')
}

export default function SignaturePadDrawOrTypeExample() {
  const [mode, setMode] = React.useState('draw')
  const [typed, setTyped] = React.useState('')
  const [drawn, setDrawn] = React.useState(false)
  const [submitted, setSubmitted] = React.useState('')
  return (
    <Form
      className="stack"
      onFormSubmit={(_values, details) =>
        setSubmitted(sent(details.event.target as HTMLFormElement))
      }
    >
      <Tabs.Root className="tabs" value={mode} onValueChange={(next) => setMode(String(next))}>
        <Tabs.List className="tabs-list" aria-label="How to sign">
          <Tabs.Tab className="tab" value="draw">
            Draw
          </Tabs.Tab>
          <Tabs.Tab className="tab" value="type">
            Type
          </Tabs.Tab>
          <Tabs.Indicator className="tabs-indicator" />
        </Tabs.List>
        <Tabs.Panel className="tabs-panel" value="draw" keepMounted>
          <Field.Root name="signature" className="field">
            <Field.Label>Signature</Field.Label>
            <SignaturePad.Root
              className="signature-pad"
              required={mode === 'draw' && typed === ''}
              onValueChange={(value) => setDrawn(value.length > 0)}
            >
              <SignaturePad.Control className="signature-pad-control">
                <SignaturePad.Guide className="signature-pad-guide">×</SignaturePad.Guide>
                <SignaturePad.Strokes />
              </SignaturePad.Control>
              <SignaturePad.Undo className="button signature-pad-button">Undo</SignaturePad.Undo>
              <SignaturePad.Redo className="button signature-pad-button">Redo</SignaturePad.Redo>
              <SignaturePad.Clear className="button signature-pad-button">Clear</SignaturePad.Clear>
            </SignaturePad.Root>
            <Field.Error className="error" match="valueMissing">
              Sign here, or type your name instead.
            </Field.Error>
          </Field.Root>
        </Tabs.Panel>
        <Tabs.Panel className="tabs-panel" value="type" keepMounted>
          <Field.Root name="typedName" className="field">
            <Field.Label>Full name</Field.Label>
            <Field.Control
              className="input"
              autoComplete="name"
              required={mode === 'type' && !drawn}
              value={typed}
              onValueChange={(next) => setTyped(String(next))}
            />
            <Field.Description className="muted">
              Your typed name signs the form as a drawn signature would.
            </Field.Description>
            <Field.Error className="error" match="valueMissing">
              Type your name, or draw your signature instead.
            </Field.Error>
          </Field.Root>
        </Tabs.Panel>
      </Tabs.Root>
      <button className="button" type="submit">
        Sign
      </button>
      {submitted ? <pre className="output">{submitted}</pre> : null}
    </Form>
  )
}

Export

Export SVG and Export PNG show the SVG markup and the PNG data URL that actionsRef exports, trimmed to the strokes.

SignaturePadExport.tsx
import * as React from 'react'
import { SignaturePad } from '@hamur/react/signature-pad'

export default function SignaturePadExportExample() {
  const actionsRef = React.useRef<SignaturePad.Root.Actions | null>(null)
  const [svg, setSvg] = React.useState('')
  const [png, setPng] = React.useState('')
  return (
    <div className="stack">
      <SignaturePad.Root className="signature-pad" actionsRef={actionsRef}>
        <SignaturePad.Control className="signature-pad-control" aria-label="Signature">
          <SignaturePad.Guide className="signature-pad-guide">×</SignaturePad.Guide>
          <SignaturePad.Strokes />
        </SignaturePad.Control>
        <SignaturePad.Clear className="button signature-pad-button">Clear</SignaturePad.Clear>
      </SignaturePad.Root>
      <div className="row">
        <button
          className="button"
          type="button"
          onClick={() => setSvg(actionsRef.current?.toSVG({ padding: 8 }) || 'The pad is empty.')}
        >
          Export SVG
        </button>
        <button
          className="button"
          type="button"
          onClick={() => setPng(actionsRef.current?.toDataURL({ padding: 8 }) ?? '')}
        >
          Export PNG
        </button>
      </div>
      {png ? <img className="signature-pad-image" src={png} alt="The signature as PNG" /> : null}
      {svg ? <pre className="output signature-pad-svg">{svg}</pre> : null}
    </div>
  )
}

API reference

19 props across 7 parts, in React syntax. className, style, render are listed once, under Shared props.

ClearSignaturePad.Clear

Clears the strokes, as one step Undo can take back. Named "Clear" and the Field label; aria-disabled while there is nothing to do. Renders a <button>.

Clear props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
focusableWhenDisabledWhether the button stays focusable while unavailable, so focus stays on it after a press.booleantrue

ControlSignaturePad.Control

The surface drawn on: an application named by the Field label and described by its state ("Empty" or "Signed"), the Field description and the error. Holds the Strokes and the Guide. Renders a <div>.

GuideSignaturePad.Guide

A presentational hint inside the Control, such as a baseline or an "×", hidden from assistive technology. Instructions a reader needs belong in Field.Description. Renders a <div>.

RedoSignaturePad.Redo

Puts back the step Undo took back. Named "Redo" and the Field label; aria-disabled while there is nothing to do. Renders a <button>.

Redo props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
focusableWhenDisabledWhether the button stays focusable while unavailable, so focus stays on it after a press.booleantrue

RootSignaturePad.Root

Groups all parts of the signature pad: a surface drawn on with a mouse, a finger or a pen, and the buttons that clear, undo and redo the strokes. with the hidden input, followed by the status region. Renders a <div>.

Root props
PropTypeDefault
valueThe strokes.SignaturePadValue—
defaultValueThe strokes when the component is initially rendered.SignaturePadValue—
onValueChangeEvent handler called once per stroke, Clear, Undo, Redo, shortcut, imperative action and((value: SignaturePadValue, eventDetails: SignaturePadRootChangeEventDetails) => void)—
minStrokeWidthThe ink width in CSS pixels at pressure 0.number1
maxStrokeWidthThe ink width in CSS pixels at pressure 1.number3
formatWhat the hidden input submits: an image as a data: URL, or the strokes as JSON.SignaturePadFormat'png'
disabledWhether the component should ignore user interaction.booleanfalse
readOnlyWhether the user should be unable to change the strokes.booleanfalse
requiredWhether a signature is required before submitting a form.booleanfalse
nameIdentifies the field when a form is submitted.string—
formIdentifies the form that owns the hidden input.string—
actionsRefA ref to clear, undo, redo and export from the app.React.RefObject<SignaturePadRootActions | null>—
labelsThe texts, in English by default: the role description, the state texts, the buttons' namesPartial<SignaturePadLabels>—

StrokesSignaturePad.Strokes

The ink: one path per stroke, filled with currentColor, the stroke in progress last with data-drawing. Place it inside the Control. Renders a <svg>.

UndoSignaturePad.Undo

Takes back the last stroke, Clear or Redo. Named "Undo" and the Field label; aria-disabled while there is nothing to do. Renders a <button>.

Undo props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
focusableWhenDisabledWhether the button stays focusable while unavailable, so focus stays on it after a press.booleantrue

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
ControlzMetazUndoes the last stroke, Clear or RedoControl
ControlShiftzControlyRedoes the step that was undoneControl
DeleteBackspaceClears the strokes, as one step Undo can take backControl
EscapeDuring a stroke, drops it and keeps a Dialog around the pad openControl
EscapeWithout a stroke, is left to what is around the padControl
ControlzHeld down, undoes step by stepControl
EnterSpaceUndoes, and keeps focus on the button when nothing is left to undoUndo
TabReaches Clear and Undo on an empty pad too, where they are aria-disabledClear

Accessibility

What the parts set
PartSetsSo that
Controlrole="application" with the role description "signature pad", one Tab stop, named by the Field's label, described by a hidden "Empty" or "Signed" (and "Required"), the Field's description and errora screen reader passes the pointer through, says what the surface is, whether it is signed and what to do
Strokesaria-hidden="true" and focusable="false" on the svgthe ink is a picture of the value, not something to read
Guidearia-hidden="true"the baseline and its "×" are not read
Cleara button named "Clear" and the Field's label ("Clear Signature"), aria-disabled and still focusable while the pad is emptyfocus stays on the button after it has done its work, and its name says which pad it clears
Undothe same with "Undo", aria-disabled while there is nothing to undothe same
Redothe same with "Redo", aria-disabled until something is undonethe same
Rootno role; the hidden input of the form and a polite status after it that says "Signature cleared", "Undone" or "Redone"a change made with a button or a key is announced once, as focus does not move to the strokes

Focus order

  1. The Control is one Tab stop, then Clear, Undo and Redo in their order in the page, each focusable while unavailable.
  2. A press on the Control focuses it without scrolling the page, so a shortcut works right after a stroke; a press on the Field's label focuses it too.
  3. Clear, Undo and Redo leave focus where it is, and nothing takes focus on mount or on a form reset.