Skip to main content

How to Build a Signature Pad with JavaScript Canvas

A signature pad is a common UI component for capturing digital signatures in web forms, contracts, and e-signing workflows. Konva makes it easy to build one with smooth line drawing, customizable pen settings, and one-click export to PNG.

Instructions: Draw your signature below. Use the controls to change pen color, clear the pad, or save/export the signature as a PNG image.

import Konva from 'konva';

// --- UI Controls ---
const controls = document.createElement('div');
controls.style.cssText = 'display:flex;gap:8px;align-items:center;margin-bottom:4px;flex-wrap:wrap;';

const colorLabel = document.createElement('label');
colorLabel.textContent = 'Pen Color: ';
const colorInput = document.createElement('input');
colorInput.type = 'color';
colorInput.value = '#000000';
colorLabel.appendChild(colorInput);

const widthLabel = document.createElement('label');
widthLabel.textContent = 'Width: ';
const widthSelect = document.createElement('select');
[2, 3, 4, 5, 6].forEach((w) => {
  const opt = document.createElement('option');
  opt.value = w;
  opt.textContent = w + 'px';
  if (w === 3) opt.selected = true;
  widthSelect.appendChild(opt);
});
widthLabel.appendChild(widthSelect);

const clearBtn = document.createElement('button');
clearBtn.textContent = 'Clear';
const saveBtn = document.createElement('button');
saveBtn.textContent = 'Save as PNG';

controls.appendChild(colorLabel);
controls.appendChild(widthLabel);
controls.appendChild(clearBtn);
controls.appendChild(saveBtn);
const container = document.getElementById('container');
container.parentNode.insertBefore(controls, container);

// --- Stage Setup ---
const width = window.innerWidth;
const height = window.innerHeight - 40;

const stage = new Konva.Stage({
  container: 'container',
  width: width,
  height: height,
});

const bgLayer = new Konva.Layer();
stage.add(bgLayer);

// signature line at bottom
const signLine = new Konva.Line({
  points: [40, height - 60, width - 40, height - 60],
  stroke: '#ccc',
  strokeWidth: 1,
  dash: [6, 4],
});
bgLayer.add(signLine);

const signLabel = new Konva.Text({
  x: 40,
  y: height - 50,
  text: 'Sign here',
  fontSize: 14,
  fill: '#aaa',
  fontFamily: 'Arial',
});
bgLayer.add(signLabel);

const drawLayer = new Konva.Layer();
stage.add(drawLayer);

let isPaint = false;
let lastLine;
let lastPointerPosition;

stage.on('mousedown touchstart', function (e) {
  isPaint = true;
  lastPointerPosition = stage.getPointerPosition();
  lastLine = new Konva.Line({
    stroke: colorInput.value,
    strokeWidth: parseInt(widthSelect.value),
    lineCap: 'round',
    lineJoin: 'round',
    tension: 0.3,
    points: [lastPointerPosition.x, lastPointerPosition.y],
  });
  drawLayer.add(lastLine);
});

stage.on('mouseup touchend', function () {
  isPaint = false;
});

stage.on('mousemove touchmove', function (e) {
  if (!isPaint) return;
  e.evt.preventDefault();
  const pos = stage.getPointerPosition();
  const newPoints = lastLine.points().concat([pos.x, pos.y]);
  lastLine.points(newPoints);
  lastPointerPosition = pos;
});

clearBtn.addEventListener('click', function () {
  drawLayer.destroyChildren();
});

saveBtn.addEventListener('click', function () {
  // temporarily hide background elements for clean export
  bgLayer.hide();
  const dataURL = stage.toDataURL({ pixelRatio: 2 });
  bgLayer.show();

  const link = document.createElement('a');
  link.download = 'signature.png';
  link.href = dataURL;
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
});