Signature

Capture handwritten signatures with mouse, touch, or stylus - with undo/redo, colour and width controls, SVG/PNG export, and type-your-name fallback

Overview

The Signature component provides a canvas-based pad for capturing handwritten signatures. It uses the Pointer Events API to support mouse, touch, and stylus input with optional pressure sensitivity. Strokes are stored as normalised co-ordinates so signatures scale correctly when the browser is resized.

Key Features

  • Pointer Events - mouse, touch, and stylus all work the same way; pressure-sensitive line width on stylus
  • Smooth curves - midpoint quadratic Bézier algorithm prevents jagged strokes
  • Undo / Redo - full stroke-level history; clear() is also undoable
  • Dual export - PNG data URL or SVG (rebuilt from stored strokes)
  • Responsive - ResizeObserver reflows strokes when the container changes width
  • Type fallback - cursive-font text entry alternative for keyboard users
  • Form integration - hidden input auto-populated; works with F.create() blueprints
  • Config Engine - declarative init via $.setup()

Live Demo

Live Demo
Exported signature Interact with the pad above, then click a button.
<div id="sig-overview" style="max-width: 560px;"></div>
<div class="flex gap-3 mt-3 flex-wrap">
    <button type="button" id="overview-export-btn" class="btn btn-primary btn-sm" data-on-click="exportPng">Export PNG</button>
    <button type="button" id="overview-isEmpty-btn" class="btn btn-outline btn-sm" data-on-click="checkEmpty">isEmpty()</button>
</div>
<div id="overview-output" class="sig-output">
    <img data-if="image.value" data-bind-src="image.value" alt="Exported signature" style="max-height:80px;background:#fff;color:var(--dm-gray-900);border-radius:4px;padding:4px;">
    <span data-if="!image.value" data-bind-text="message.value">Interact with the pad above, then click a button.</span>
</div>
/**
 * Live Demo - from public/showcase/elements/signature.
 *
 * The output panel is bound: it shows the exported PNG when there is one,
 * otherwise the last message.
 */
export function mount(root) {
    const overviewSig = E.signature(root.querySelector('#sig-overview'), {
        label: 'Signature Pad',
        placeholder: 'Draw your signature here',
    });

    const image = M.observable('');
    const message = M.observable('Interact with the pad above, then click a button.');

    const handle = M.applyBindings({
        image, message,
        exportPng() {
            if (overviewSig.isEmpty()) {
                E.toast('Pad is empty - draw something first.', {type: 'warning'});
                return;
            }
            image.value = overviewSig.toBase64('png');
        },
        checkEmpty() {
            image.value = '';
            message.value = `isEmpty() → ${overviewSig.isEmpty()}`;
        }
    }, root);

    return () => {
        handle.dispose();
        overviewSig?.destroy?.();
    };
}

Getting Started

Create an empty container element and pass its selector to E.signature().

Step 1: HTML

<div id="my-signature"></div>

Step 2: JavaScript

const sig = E.signature('#my-signature', {
    label: 'Your Signature',
    name: 'signature',           // Name attribute on the hidden input
    guideLine: true,             // Show a dashed baseline
    onChange: (base64) => {
        console.log('Updated:', base64.slice(0, 40) + '...');
    }
});

The component renders a toolbar, canvas, guide line, and footer inside the container. A hidden <input type="hidden"> is automatically kept in sync so the base64 value is included in any surrounding <form> submission.

Pen Options

Configure the default pen colour and width, and the swatches shown in the toolbar. Pen width values are also used for the dot indicators in the toolbar.

Pen Options

Custom colour swatches

Thin pen, no guide line

<div class="demo-grid mb-5"><div>
        <p class="demo-label">Custom colour swatches</p>
        <div id="sig-colours" style="max-width: 440px;"></div>
    </div>
    <div>
        <p class="demo-label">Thin pen, no guide line</p>
        <div id="sig-thin" style="max-width: 440px;"></div>
    </div>
</div>
/**
 * Pen Options - from public/showcase/elements/signature.
 */
export function mount(root) {
        // ── Section 3: Pen Options ───────────────────────────────────────
    E.signature(root.querySelector('#sig-colours'), {
          penColour: '#1e40af',
          colours: ['#000000', '#1e40af', '#15803d', '#b91c1c', '#7c3aed', '#ea580c'],
          widths: [1, 2, 4],
        });
    E.signature(root.querySelector('#sig-thin'), {
          penColour: '#111',
          penWidth: 1,
          widths: [1],
          guideLine: false,
          placeholder: 'Sign with a fine nib',
        });
}
// Custom colour swatches + default to blue
E.signature('#sig-colours', {
    penColour: '#1e40af',
    colours: ['#000000', '#1e40af', '#15803d', '#b91c1c', '#7c3aed', '#ea580c'],
    widths: [1, 2, 4],
});

// Single thin pen
E.signature('#sig-thin', {
    penColour: '#111',
    penWidth: 1,
    widths: [1],
    guideLine: false,
    placeholder: 'Sign with a fine nib',
});

Export Formats

Call sig.toBase64() to export as a PNG data URL (default) or sig.toBase64('svg') to get an SVG data URL rebuilt from the stored strokes. The SVG export uses the same Bézier algorithm as the canvas renderer, so output is visually identical.

Export Formats
<div id="sig-export" style="max-width: 560px;"></div>
<div class="flex gap-3 mt-3 flex-wrap">
    <button type="button" id="export-png-btn" class="btn btn-primary btn-sm">Export PNG</button>
    <button type="button" id="export-svg-btn" class="btn btn-secondary btn-sm">Export SVG</button>
</div>
<img id="export-preview" class="sig-preview-img" src="" alt="Signature preview" style="display:none; max-height:120px;">
<p id="export-label" class="text-xs text-muted mt-1" style="display:none;"></p>
/**
 * Export Formats - from public/showcase/elements/signature.
 */
export function mount(root) {
        // ── Section 4: Export Formats ────────────────────────────────────
    const exportSig = E.signature(root.querySelector('#sig-export'), {
          label: 'Export Demo',
        });
    function showExport(format) {
          if (exportSig.isEmpty()) {
            E.toast('Draw a signature first.', { type: 'warning' });
            return;
          }
          const data = exportSig.toBase64(format);
          $(root).find('#export-preview').attr('src', data).show();
          $(root).find('#export-label').text(`Format: ${format.toUpperCase()} | Length: ${data.length} chars`).show();
        }
    $(root).find('#export-png-btn').on('click', () => showExport('png'));
    $(root).find('#export-svg-btn').on('click', () => showExport('svg'));
}
const sig = E.signature('#my-pad', { format: 'png' });

// Get PNG data URL (respects instance format)
const png = sig.toBase64();

// Override per-call
const svg = sig.toBase64('svg');

// Use in an <img> tag
document.querySelector('#preview').src = png;

Undo, Redo & Clear

Each completed stroke is pushed onto a history stack. The toolbar undo/redo buttons operate on individual strokes. clear() moves all strokes to the redo stack, so the entire pad can be restored.

Undo, Redo & Clear
<div id="sig-undo" style="max-width: 560px;"></div>
<div class="flex gap-3 mt-3 flex-wrap">
    <button type="button" id="undo-btn" class="btn btn-outline btn-sm">sig.undo()</button>
    <button type="button" id="redo-btn" class="btn btn-outline btn-sm">sig.redo()</button>
    <button type="button" id="clear-btn" class="btn btn-danger btn-sm btn-outline">sig.clear()</button>
</div>
/**
 * Undo, Redo & Clear - from public/showcase/elements/signature.
 */
export function mount(root) {
        // ── Section 5: Undo / Redo ───────────────────────────────────────
    const undoSig = E.signature(root.querySelector('#sig-undo'), {
          label: 'Undo / Redo Demo',
        });
    $(root).find('#undo-btn').on('click', () => undoSig.undo());
    $(root).find('#redo-btn').on('click', () => undoSig.redo());
    $(root).find('#clear-btn').on('click', () => undoSig.clear());
}
const sig = E.signature('#my-pad');

sig.undo();      // Remove last stroke (moves to redo stack)
sig.redo();      // Restore last undone stroke
sig.clear();     // Clear all strokes (undoable - undo restores them all)
sig.clear(true); // Silent clear - cannot be undone, no callbacks fired

Type Fallback

Enable typeFallback: true to show a Draw / Type toggle in the toolbar. In Type mode, users type their name into a text input rendered in a cursive web-safe font. The text is rendered to the canvas so toBase64() still returns an image.

Type Fallback
<div id="sig-type" style="max-width: 560px;"></div>
/**
 * Type Fallback - from public/showcase/elements/signature.
 */
export function mount(root) {
        // ── Section 6: Type Fallback ─────────────────────────────────────
    E.signature(root.querySelector('#sig-type'), {
          label: 'Draw or Type',
          typeFallback: true,
          placeholder: 'Switch to Type mode to type your name',
        });
}
E.signature('#my-pad', {
    typeFallback: true,   // Show Draw / Type toggle in toolbar
    placeholder: 'Sign here or switch to Type mode',
});

Disabled State

Use disabled: true in options or call sig.disable() / sig.enable() at runtime to lock or unlock the pad.

Disabled State
<div id="sig-disabled" style="max-width: 560px;"></div>
<div class="flex gap-3 mt-3">
    <button type="button" id="enable-btn" class="btn btn-primary btn-sm">sig.enable()</button>
    <button type="button" id="disable-btn" class="btn btn-outline btn-sm">sig.disable()</button>
</div>
/**
 * Disabled State - from public/showcase/elements/signature.
 */
export function mount(root) {
        // ── Section 7: Disabled ──────────────────────────────────────────
    const disabledSig = E.signature(root.querySelector('#sig-disabled'), {
          label: 'Locked Pad',
          disabled: true,
          placeholder: 'This pad is currently locked',
        });
    $(root).find('#enable-btn').on('click', () => disabledSig.enable());
    $(root).find('#disable-btn').on('click', () => disabledSig.disable());
}
const sig = E.signature('#my-pad', { disabled: true });

sig.enable();   // Allow drawing again
sig.disable();  // Lock the pad (canvas becomes non-interactive)

Form Integration

Use type: 'signature' in a Domma blueprint to include a signature field in any F.create() form. The component syncs automatically with the form model, and the hidden input value is included when the form is submitted.

Blueprint

const contractBlueprint = {
    name: {
        type: M.types.string,
        required: true,
        label: 'Full Name',
    },
    agreed: {
        type: M.types.boolean,
        label: 'I agree to the terms & conditions',
        required: true,
    },
    signature: {
        type: 'signature',
        label: 'Signature',
        required: true,
    }
};

F.create('#contract-form', {
    blueprint: contractBlueprint,
    onSubmit: (data) => {
        console.log('Name:', data.name);
        console.log('Agreed:', data.agreed);
        console.log('Signature (PNG):', data.signature.slice(0, 40));
    }
});

Plain HTML Form

<!-- The hidden input is rendered inside the component -->
<form id="my-form">
    <input type="text" name="name" placeholder="Your name">
    <div id="my-pad"></div>
    <!-- #my-pad will contain: <input type="hidden" name="signature"> -->
    <button type="submit">Submit</button>
</form>

<script type="module">
    E.signature('#my-pad', { name: 'signature' });

    $('#my-form').on('submit', (e) => {
        e.preventDefault();
        const data = new FormData(e.target);
        console.log(data.get('signature')); // base64 PNG
    });
</script>

Config Engine

Initialise signature pads declaratively using $.setup(). This is useful for MPA pages where you want to configure components in a central JavaScript file without importing the element constructor directly.

$.setup({
    '#contract-sig': {
        component: 'signature',
        options: {
            label: 'Client Signature',
            penColour: '#1a1a2e',
            guideLine: true,
            typeFallback: true,
            onChange: (base64) => {
                console.log('Signature updated');
            }
        }
    }
});

Tutorial - Contract Signing Form

Build a contract signing widget: the user fills in their name, ticks a consent checkbox, signs the pad, and the complete form data (including the base64 signature) is displayed for submission.

Step 1: HTML

<form id="contract-form">
    <div class="mb-4">
        <label class="form-label">Full Name</label>
        <input type="text" class="form-input" name="fullName" required>
    </div>
    <div class="mb-4">
        <label class="form-check-label">
            <input type="checkbox" name="agreed" required>
            I agree to the terms &amp; conditions
        </label>
    </div>
    <div class="mb-4">
        <label class="form-label">Signature</label>
        <div id="contract-sig"></div>
    </div>
    <button type="submit" class="btn btn-primary">Sign &amp; Submit</button>
</form>

Step 2: JavaScript

const sig = E.signature('#contract-sig', {
    label: 'Your Signature',
    name: 'signature',
    typeFallback: true,
    onChange: () => {
        submitBtn.disabled = sig.isEmpty();
    }
});

const submitBtn = document.querySelector('[type="submit"]');
submitBtn.disabled = true; // require signature

$('#contract-form').on('submit', async (e) => {
    e.preventDefault();

    if (sig.isEmpty()) {
        E.toast('Please add your signature.', { type: 'warning' });
        return;
    }

    const formData = new FormData(e.target);
    const payload = {
        fullName:  formData.get('fullName'),
        agreed:    formData.get('agreed') === 'on',
        signature: sig.toBase64('png'),
    };

    await E.confirm('Submit this signed contract?');
    console.log('Submitted:', payload);
    E.toast('Contract signed successfully!', { type: 'success' });
});

Step 3: Live Demo

Step 3: Live Demo
<form id="tutorial-contract-form" style="max-width: 560px;">
    <div class="mb-4">
        <label class="form-label">Full Name</label>
        <input type="text" class="form-input" name="fullName" placeholder="Jane Smith" required="">
    </div>
    <div class="mb-4">
        <label style="display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
            <input type="checkbox" name="agreed" id="tutorial-agreed">
            I agree to the terms &amp; conditions
        </label>
    </div>
    <div class="mb-4">
        <label class="form-label">Signature</label>
        <div id="tutorial-sig"></div>
    </div>
    <button type="submit" id="tutorial-submit-btn" class="btn btn-primary" disabled="">Sign &amp; Submit</button>
</form>
<div id="tutorial-output" class="sig-output mt-4" style="display:none;"></div>
/**
 * Step 3: Live Demo - from public/showcase/elements/signature.
 */
export function mount(root) {
        // ── Section 10: Tutorial ─────────────────────────────────────────
    const tutSig = E.signature(root.querySelector('#tutorial-sig'), {
          label: 'Your Signature',
          name: 'signature',
          typeFallback: true,
          onChange: () => {
            $(root).find('#tutorial-submit-btn').prop('disabled', tutSig.isEmpty());
          }
        });
    $(root).find('#tutorial-contract-form').on('submit', async (e) => {
          e.preventDefault();
          if (tutSig.isEmpty()) {
            E.toast('Please add your signature before submitting.', { type: 'warning' });
            return;
          }
          const agreed = $(root).find('#tutorial-agreed').prop('checked');
          if (!agreed) {
            E.toast('You must agree to the terms.', { type: 'warning' });
            return;
          }
          const fullName = e.target.querySelector('[name="fullName"]').value.trim();
          const confirmed = await E.confirm('Submit this signed contract?');
          if (!confirmed) return;

          const b64 = tutSig.toBase64('png');
          const output = `
            <strong>Submitted!</strong><br>
            Name: ${_.escapeHtml ? _.escapeHtml(fullName) : fullName}<br>
            Agreed: ${agreed}<br>
            Signature: <img src="${b64}" style="height:40px;vertical-align:middle;background:#fff;color:var(--dm-gray-900);border-radius:4px;padding:2px;margin-left:4px;">
          `;
          $(root).find('#tutorial-output').html(output).show();
          E.toast('Contract signed successfully!', { type: 'success' });
        });
}

JavaScript API

Initialisation

const sig = E.signature('#el', {
    // Layout
    height: 180,              // Canvas height in px (default: 180)
    label: 'Signature',       // Toolbar label (default: 'Signature')
    placeholder: 'Sign here', // Placeholder text (default: 'Sign here')
    guideLine: true,          // Show dashed guide line (default: true)
    toolbar: true,            // Show/hide toolbar (default: true)

    // Pen
    penColour: '#000000',     // Default pen colour (default: '#000000')
    penWidth: 2,              // Default pen width in px (default: 2)
    colours: ['#000', '#1e40af', '#15803d', '#b91c1c'],  // Colour swatches
    widths: [1, 2, 4],        // Width options

    // Output
    format: 'png',            // 'png' | 'svg' (default: 'png')
    name: 'signature',        // Hidden input name attribute (default: 'signature')

    // Behaviour
    disabled: false,          // Start disabled (default: false)
    typeFallback: false,      // Show Draw / Type toggle (default: false)
    minStrokeLength: 3,       // Min pointer points to register a stroke (default: 3)
    respectMotionPreference: true, // Honour prefers-reduced-motion (default: true)

    // Callbacks
    onChange: (base64) => {}, // Fired after every stroke or type input
    onClear:  ()       => {}, // Fired when clear() is called (non-silent)
    onBegin:  (stroke) => {}, // Fired on pointerdown (stroke in progress)
    onEnd:    (stroke) => {}, // Fired on pointerup (stroke committed)
});

Instance Methods

sig.toBase64()          // Export as PNG data URL (uses instance format)
sig.toBase64('png')     // Export as PNG data URL
sig.toBase64('svg')     // Export as SVG data URL (rebuilt from stored strokes)

sig.isEmpty()           // → boolean - true if no strokes drawn
sig.clear()             // Clear all strokes (undoable)
sig.clear(true)         // Silent clear - no redo history, no callbacks
sig.undo()              // Remove last stroke → move to redo stack
sig.redo()              // Restore last undone stroke
sig.disable()           // Lock the pad
sig.enable()            // Unlock the pad
sig.destroy()           // Remove all events and disconnect ResizeObserver

Options Reference

Option Type Default Description
height number 180 Canvas height in px
label string 'Signature' Toolbar label
placeholder string 'Sign here' Canvas placeholder text
guideLine boolean true Show dashed baseline
toolbar boolean true Show/hide entire toolbar
penColour string '#000000' Default pen colour (CSS colour value)
penWidth number 2 Default pen width in px
colours string[] ['#000',…] Colour swatches in toolbar
widths number[] [1,2,4] Width options in toolbar
format 'png'|'svg' 'png' Default export format
name string 'signature' Hidden input name attribute
disabled boolean false Start in disabled state
typeFallback boolean false Show Draw/Type mode toggle
minStrokeLength number 3 Minimum pointer events to commit a stroke
onChange function null Called with base64 after each change
onClear function null Called after a non-silent clear
onBegin function null Called on pointerdown with stroke object
onEnd function null Called on pointerup with stroke object

Accessibility

  • role="application" on the container with an aria-label
  • All toolbar buttons use <button> with aria-label and aria-pressed
  • The status bar uses aria-live="polite" to announce state changes (Signing…, Signature captured, Signature cleared)
  • typeFallback: true provides a keyboard-accessible alternative to drawing
  • tabindex="0" on the canvas allows focus; use :focus-visible ring for visibility
  • respectMotionPreference: true (default) disables transitions for users who prefer reduced motion

Related Elements

Related Elements
<a href="/showcase/elements" class="card card-hover">
    <div class="card-body">
        <h3 class="card-title">All Elements</h3>
        <p class="card-text">Browse every UI component in Domma Elements</p>
    </div>
</a>
<a href="/showcase/elements/pillbox" class="card card-hover">
    <div class="card-body">
        <h3 class="card-title">Pillbox</h3>
        <p class="card-text">Multi-select tag input - another specialist form component</p>
    </div>
</a>
<a href="/showcase/elements/modal" class="card card-hover">
    <div class="card-body">
        <h3 class="card-title">Modal</h3>
        <p class="card-text">Present a signature pad inside a confirmation dialog</p>
    </div>
</a>