signature_pad v5 reference — electronic signature capture for enrollment forms and documents in A3
From a3-pluginnpx claudepluginhub trusted-american/marketplace --plugin a3-pluginThis skill uses the workspace's default tool permissions.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
HTML5 canvas-based smooth signature drawing. Used in A3 for enrollment forms, compliance documents, and any workflow requiring electronic signatures.
pnpm add signature_pad
import SignaturePad from 'signature_pad';
const canvas = document.querySelector<HTMLCanvasElement>('#signature-canvas');
const signaturePad = new SignaturePad(canvas, {
// Drawing options
penColor: 'rgb(0, 0, 0)', // Default: 'black'
backgroundColor: 'rgb(255, 255, 255)', // Default: 'rgba(0,0,0,0)' (transparent)
minWidth: 0.5, // Minimum stroke width in px
maxWidth: 2.5, // Maximum stroke width in px
velocityFilterWeight: 0.7, // How much velocity affects width (0-1)
throttle: 16, // Max ms between points (~60fps)
minDistance: 5, // Min distance (px) between points
dotSize: 0, // Radius of dot on tap (0 = calculated from min/maxWidth)
compositeOperation: 'source-over', // Canvas composite operation
});
| Option | Type | Default | Description |
|---|---|---|---|
penColor | string | 'black' | CSS color for the pen stroke |
backgroundColor | string | 'rgba(0,0,0,0)' | Background color (set white for JPEG/PDF export) |
minWidth | number | 0.5 | Minimum line width |
maxWidth | number | 2.5 | Maximum line width |
velocityFilterWeight | number | 0.7 | Weight for velocity smoothing |
throttle | number | 16 | Throttle drawing to ms (0 = no throttle) |
minDistance | number | 5 | Minimum distance between points to record |
dotSize | number | 0 | Size of single-tap dots |
compositeOperation | string | 'source-over' | Canvas globalCompositeOperation |
// Export as PNG (default)
const pngDataUrl = signaturePad.toDataURL();
// => 'data:image/png;base64,...'
// Export as JPEG with quality
const jpegDataUrl = signaturePad.toDataURL('image/jpeg', 0.8);
// Export as SVG
const svgDataUrl = signaturePad.toDataURL('image/svg+xml');
const svgString = signaturePad.toSVG();
// Returns raw SVG markup: '<svg xmlns="..." ...>...</svg>'
// With custom options
const svgString = signaturePad.toSVG({ includeBackgroundColor: true });
const pointGroups = signaturePad.toData();
// Returns: PointGroup[] where each group is a stroke
// Each point: { x: number, y: number, pressure: number, time: number }
// Load a previously saved signature
await signaturePad.fromDataURL(dataUrl);
// With size options (scales the image)
await signaturePad.fromDataURL(dataUrl, {
width: 400,
height: 200,
ratio: 1, // Pixel ratio override
});
Important: fromDataURL is async. Always await it before doing further operations.
// Restore from saved point data
signaturePad.fromData(pointGroups);
// Append to existing drawing instead of replacing
signaturePad.fromData(pointGroups, { clear: false });
signaturePad.clear();
// Clears all strokes and resets to backgroundColor
if (signaturePad.isEmpty()) {
alert('Please provide a signature before submitting.');
}
// Disable (read-only mode)
signaturePad.off();
// Re-enable
signaturePad.on();
signaturePad.addEventListener('beginStroke', (event) => {
// User started drawing a stroke
console.log('Stroke started');
});
signaturePad.addEventListener('endStroke', (event) => {
// User finished a stroke
console.log('Stroke ended');
autoSave();
});
signaturePad.addEventListener('beforeUpdateStroke', (event) => {
// Called before each point is added
});
signaturePad.addEventListener('afterUpdateStroke', (event) => {
// Called after each point is added
});
The canvas must be properly sized for high-DPI displays. Without this, signatures look blurry on retina screens and coordinates are wrong.
function resizeCanvas(canvas: HTMLCanvasElement, signaturePad: SignaturePad) {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const width = canvas.offsetWidth;
const height = canvas.offsetHeight;
canvas.width = width * ratio;
canvas.height = height * ratio;
const ctx = canvas.getContext('2d');
ctx?.scale(ratio, ratio);
// Must clear after resize — canvas content is lost
signaturePad.clear();
}
// Initial setup
resizeCanvas(canvas, signaturePad);
// Handle window resize
window.addEventListener('resize', () => {
// Save current data
const data = signaturePad.toData();
resizeCanvas(canvas, signaturePad);
// Restore data after resize
signaturePad.fromData(data);
});
const observer = new ResizeObserver(() => {
const data = signaturePad.toData();
resizeCanvas(canvas, signaturePad);
signaturePad.fromData(data);
});
observer.observe(canvas.parentElement!);
// Cleanup
observer.disconnect();
A3 wraps signature_pad in a Glimmer component:
// app/components/signature-capture.gts
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
import { modifier } from 'ember-modifier';
import SignaturePad from 'signature_pad';
interface SignatureCaptureSignature {
Args: {
onSave: (dataUrl: string) => void;
existingSignature?: string;
readonly?: boolean;
penColor?: string;
};
Element: HTMLCanvasElement;
}
export default class SignatureCapture extends Component<SignatureCaptureSignature> {
@tracked signaturePad: SignaturePad | null = null;
@tracked hasSignature = false;
setupPad = modifier((canvas: HTMLCanvasElement) => {
const pad = new SignaturePad(canvas, {
penColor: this.args.penColor ?? 'rgb(0, 0, 0)',
backgroundColor: 'rgb(255, 255, 255)',
minWidth: 0.5,
maxWidth: 2.5,
});
this.signaturePad = pad;
// High-DPI setup
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext('2d')?.scale(ratio, ratio);
// Load existing signature if provided
if (this.args.existingSignature) {
pad.fromDataURL(this.args.existingSignature);
this.hasSignature = true;
}
if (this.args.readonly) {
pad.off();
}
pad.addEventListener('endStroke', () => {
this.hasSignature = !pad.isEmpty();
});
// Cleanup
return () => {
pad.off();
};
});
@action clear() {
this.signaturePad?.clear();
this.hasSignature = false;
}
@action save() {
if (this.signaturePad && !this.signaturePad.isEmpty()) {
const dataUrl = this.signaturePad.toDataURL('image/png');
this.args.onSave(dataUrl);
}
}
}
A3 pattern for persisting signatures via Firebase Cloud Storage:
import { getStorage, ref, uploadString, getDownloadURL } from 'firebase/storage';
async function saveSignatureToStorage(
dataUrl: string,
path: string // e.g. 'signatures/{userId}/{documentId}.png'
): Promise<string> {
const storage = getStorage();
const storageRef = ref(storage, path);
// Upload the base64 data URL directly
const snapshot = await uploadString(storageRef, dataUrl, 'data_url', {
contentType: 'image/png',
customMetadata: {
signedAt: new Date().toISOString(),
},
});
// Get the download URL for later retrieval
const downloadUrl = await getDownloadURL(snapshot.ref);
return downloadUrl;
}
// Usage in form submission
const dataUrl = signaturePad.toDataURL();
const signatureUrl = await saveSignatureToStorage(
dataUrl,
`signatures/${userId}/${enrollmentId}.png`
);
// Save URL reference in Firestore document
await updateDoc(doc(db, 'enrollments', enrollmentId), {
signatureUrl,
signedAt: serverTimestamp(),
});
function dataUrlToBlob(dataUrl: string): Blob {
const [header, base64] = dataUrl.split(',');
const mime = header.match(/:(.*?);/)?.[1] || 'image/png';
const binary = atob(base64);
const array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
array[i] = binary.charCodeAt(i);
}
return new Blob([array], { type: mime });
}
backgroundColor: 'rgb(255, 255, 255)'.canvas.width and canvas.height in JavaScript, not just CSS. CSS-only sizing makes drawing coordinates wrong.isEmpty() returns true even after fromDataURL because it only tracks drawn strokes. Use fromData or track loading state separately.