Signature Pad
Canvas signature capture that participates in forms: freehand strokes serialize to a PNG data-URL and submit as the field value.
<arc-signature-pad>Overview
SignaturePad is a bordered drawing surface for capturing a handwritten signature. While blank, a muted "Sign here" baseline invites the first stroke and disappears the moment ink goes down. The pen line scales with stroke velocity (slow movement thickens it and fast flicks thin it), so a mouse or finger produces something that reads as handwriting rather than a polyline. The pen color follows --text-primary by default and accepts any CSS color, including a var() reference, through the pen-color attribute.
The component is a real form control. Each completed stroke serializes the whole canvas to a PNG data-URL and submits it under name; a blank pad submits nothing, and required on a blank pad reports valueMissing like any other input. The stroke is the edit unit: arc-input and arc-change fire together once per completed stroke, never per point, each carrying the data-URL in event.detail.value. Once signed, a small ghost clear button appears in the top corner; clearing empties the value, fires arc-clear, and brings the placeholder back. clear() does the same thing from script: it wipes the canvas, empties the value, and restores the placeholder. Call it from your own "start over" control, or after a failed submit that should not leave a stale signature in a form the user is about to retry. toDataURL(type) exports the current image in a format other than the PNG the value carries.
Signing by hand is a pointer gesture, and the pad offers no keyboard path to produce a signature. The canvas exposes role="img" with an accessible name that announces its signed or empty state, it stays focusable, and the clear button is keyboard-reachable. But if your form must be completable without a pointer, you need to offer an equivalent alongside the pad, such as a type-to-sign text field or a file upload. The component does not simulate one.
Guidelines
When to use
- Give the pad a `label`: it doubles as the accessible name of the canvas
- Set `required` when a signature is mandatory; the pad reports `valueMissing` while blank exactly like a native input
- Offer a keyboard-accessible alternative (type-to-sign, upload) next to the pad when the form must be completable without a pointer: the pad itself is pointer-only by nature
- Listen for `arc-change` to persist or preview the signature; each completed stroke delivers the full, current image
- Use `readonly` to display a captured signature that still submits but can no longer be altered
- Call `clear()` when your own flow invalidates the signature: a changed name field, a rejected submission, a switched signer
When not to use
- Do not reflect or store the value as an attribute: a data-URL is far too large; read the `value` property or the event detail instead
- Do not treat a signature image as proof of identity on its own; pair it with real authentication when it matters legally
- Do not listen per-point for drawing progress; the component stays silent until a stroke completes
- Do not hide the clear button behind your own chrome: a signer who slips needs an obvious way to start over
- Avoid very small pads; under about 240px of width there is not enough room for a natural signing motion
Features
- Freehand drawing with pointer capture: mouse, touch, and stylus all work, and touch never scrolls the page mid-stroke
- Velocity-scaled pen width (up to 40% thicker or thinner around the `pen-width` base) with midpoint-smoothed curves
- Serializes to a PNG data-URL after every completed stroke; `toDataURL(type)` exports other formats on demand
- Full form participation: submits under `name`, `required` + blank reports `valueMissing`, `form.reset()` restores the initial state
- The stroke is the edit unit: `arc-input` and `arc-change` fire once per stroke, never per point
- Ghost clear button appears once signed; clearing fires `arc-clear` and restores the "Sign here" placeholder
- `clear()` resets the pad from script: same wipe, same `arc-clear`, same placeholder as the button
- Pen color resolves CSS custom properties at stroke time, so it follows theme changes
- Crisp on high-DPI screens: the backing store tracks `devicePixelRatio`, and completed strokes survive a resize
- Setting `value` from script draws the image back onto the canvas, so a saved signature can be restored for review
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available. Use the Web Component directly or a framework wrapper.
<form>
<arc-signature-pad
label="Signature"
name="signature"
required
></arc-signature-pad>
</form>
<script>
const pad = document.querySelector('arc-signature-pad');
// Fired once per completed stroke with the PNG data-URL
pad.addEventListener('arc-change', (e) => {
console.log('Signature updated:', e.detail.value.slice(0, 40) + '…');
});
pad.addEventListener('arc-clear', () => {
console.log('Signature cleared');
});
</script>import { SignaturePad } from '@arclux/arc-ui-react';
export default function Example() {
return (
<SignaturePad
label="Signature"
name="signature"
required
onArcChange={(e) => console.log('Signature:', e.detail.value)}
onArcClear={() => console.log('Cleared')}
/>
);
}<script setup>
import { SignaturePad } from '@arclux/arc-ui-vue';
</script>
<template>
<SignaturePad
label="Signature"
name="signature"
required
@arc-change="(e) => console.log('Signature:', e.detail.value)"
@arc-clear="() => console.log('Cleared')"
/>
</template><script>
import { SignaturePad } from '@arclux/arc-ui-svelte';
</script>
<SignaturePad
label="Signature"
name="signature"
required
on:arc-change={(e) => console.log('Signature:', e.detail.value)}
on:arc-clear={() => console.log('Cleared')}
/>import { Component } from '@angular/core';
import { SignaturePad } from '@arclux/arc-ui-angular';
@Component({
imports: [SignaturePad],
template: `
<arc-signature-pad
label="Signature"
name="signature"
required
(arc-change)="onSign($event)"
(arc-clear)="onClear()"
></arc-signature-pad>
`,
})
export class MyComponent {
onSign(e: CustomEvent) {
console.log('Signature:', e.detail.value);
}
onClear() {
console.log('Cleared');
}
}import { SignaturePad } from '@arclux/arc-ui-solid';
export default function Example() {
return (
<SignaturePad
label="Signature"
name="signature"
required
onArcChange={(e) => console.log('Signature:', e.detail.value)}
onArcClear={() => console.log('Cleared')}
/>
);
}import { SignaturePad } from '@arclux/arc-ui-preact';
export default function Example() {
return (
<SignaturePad
label="Signature"
name="signature"
required
onArcChange={(e) => console.log('Signature:', e.detail.value)}
onArcClear={() => console.log('Cleared')}
/>
);
}API
SPEED_FULLnumber1.5- Stroke speed (CSS px per ms) at which the pen reaches its thinnest.
valuestring''- The signature as a PNG data-URL, empty string while the pad is blank. Updated after every completed stroke. Setting it from script draws the image onto the canvas (client-side only). Not reflected: a data-URL is far too large to live in an attribute.
namestring''- Form field name the data-URL submits under.
labelstring''- Label text displayed above the pad in the label typography role. Also feeds the canvas's accessible name.
disabledbooleanfalse- Disables interaction, reducing opacity and blocking pointer events. The pad leaves the tab order.
pen-colorstring''- Pen color as any CSS color, including a
var()expression, resolved against the canvas at stroke time. Attribute:pen-color. Defaults to the resolved value of--text-primary. readonlybooleanfalse- Prevents drawing and hides the clear button while the pad stays focusable and the value still submits.
requiredbooleanfalse- When true and the pad is blank, the control is invalid with
valueMissing. penWidthnumber2- Base pen width in CSS pixels. The drawn line scales with stroke velocity, up to 40% thicker on slow, deliberate movement and 40% thinner on fast flicks. Attribute:
pen-width. Default 2. formAssociatedbooleantruepropertiesobject{ // flag(), unlike `disabled`. The exclusion in props.js is specifically // about form-associated *platform* semantics: a `disabled` content // attribute that is merely present makes the element actually disabled // per the HTML spec, and formDisabledCallback assigns the property back, // so no converter can win. Neither of these is platform-mapped: // `required` is enforced by _computeValidity() below and `readonly` by // each component's own interaction handlers, so the stock converter buys // nothing here and costs the usual bug: `required="false"` read as true, // blocking submission of a form the author meant to leave optional. // Finding #48's shape, across all 26 form controls at once. required: flag(false), readonly: flag(false), }- Lit merges static properties up the prototype chain, so every consumer
gets these without declaring them.
requiredparticipates in constraint validation below;readonlyreflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state). autoValidatesbooleantrue- Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
formvalidityvalidationMessage
Methods
clear()- Wipe the canvas, empty the value, and bring the placeholder back.
toDataURL(type?)type?: stringstring- The current canvas as a data-URL in the requested format (PNG by
default). Unlike
value, this reads the canvas directly, so a blank pad returns a blank image rather than an empty string. checkValidity()boolean- Whether the control currently satisfies its constraints, per the native
constraint-validation API. Fires
invalidon the element when it does not, and reports nothing to the user. reportValidity()boolean- As checkValidity(), but also shows the browser's validation message against the control when it fails.
Events
arc-clear- Fired when the pad is cleared, via the clear button or the clear() method.
arc-inputdetail:{ value: string }- Fired once per completed stroke with the serialized data-URL. A stroke is the edit unit: nothing fires per point while the pen is down.
arc-changedetail:{ value: string }- Fired when the pointer session ends and the value serializes. A stroke is a discrete gesture, so each stroke end fires arc-input then arc-change together.
See Also
- File UploadDrag-and-drop file upload zone with preview.
- Image CropperCrop-before-upload control with a draggable, resizable crop rectangle, aspect-ratio locking, zoom, and canvas export at natural image resolution.
- Color PickerColor picker with a saturation/lightness area, hue slider, hex input, and optional preset swatches.