iTranslated by AI
Building a Digital Signature UI for Deliveries in React 🚛✒️
Introduction
I would like to introduce a simple implementation of a feature (handwritten character feature) that I created as a prototype but didn't have many chances to use!
I wrote some tips about opening and closing the signature area in the following article 🙇
About the feature
This is a free-hand signature feature for implementing electronic signatures in web applications.
- Signature input
- Undo functionality
- Redo functionality
- Clear functionality
- Management of button enable/disable states
Library used
Outcome
*Screen layout details are omitted in the following explanation.

Full code for the Hooks
import { useRef, useEffect, useState, useCallback } from "react";
import SignaturePad, { PointGroup } from "signature_pad";
export const useSignature = () => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const signaturePadRef = useRef<SignaturePad | null>(null);
const signatureRedoArray = useRef<PointGroup[]>([]);
const [isUndoable, setIsUndoable] = useState(false);
const [isRedoable, setIsRedoable] = useState(false);
const updateButtonStates = useCallback(() => {
if (signaturePadRef.current) {
setIsUndoable(signaturePadRef.current.toData().length > 0);
setIsRedoable(signatureRedoArray.current.length > 0);
}
}, []);
const resizeCanvas = useCallback(() => {
const canvas = canvasRef.current;
if (canvas) {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = window.innerWidth * ratio;
canvas.height = window.innerHeight * ratio;
canvas.getContext("2d")!.scale(ratio, ratio);
if (signaturePadRef.current) {
signaturePadRef.current.clear();
}
}
}, []);
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
const signaturePad = new SignaturePad(canvas, {
backgroundColor: "rgb(255, 255, 255)",
});
signaturePadRef.current = signaturePad;
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
const handleEndStroke = () => {
updateButtonStates();
};
signaturePad.addEventListener("endStroke", handleEndStroke);
return () => {
window.removeEventListener("resize", resizeCanvas);
signaturePad.removeEventListener("endStroke", handleEndStroke);
};
}
}, [resizeCanvas, updateButtonStates]);
const undo = useCallback(() => {
if (signaturePadRef.current) {
const signatureData = signaturePadRef.current.toData();
if (signatureData.length > 0) {
const lastState = signatureData.pop();
signaturePadRef.current.fromData(signatureData);
if (lastState) {
signatureRedoArray.current.push(lastState);
}
updateButtonStates();
}
}
}, [updateButtonStates]);
const redo = useCallback(() => {
if (signaturePadRef.current && signatureRedoArray.current.length > 0) {
const lastRedo = signatureRedoArray.current.pop();
if (lastRedo) {
const signatureData = signaturePadRef.current.toData();
signatureData.push(lastRedo);
signaturePadRef.current.fromData(signatureData);
updateButtonStates();
}
}
}, [updateButtonStates]);
const clear = useCallback(() => {
if (signaturePadRef.current) {
signaturePadRef.current.clear();
signatureRedoArray.current = [];
setIsUndoable(false);
setIsRedoable(false);
}
}, []);
return {
canvasRef,
clear,
redo,
undo,
isUndoable,
isRedoable,
};
};
Implementation details
State management
const canvasRef = useRef<HTMLCanvasElement>(null);
const signaturePadRef = useRef<SignaturePad | null>(null);
const signatureRedoArray = useRef<PointGroup[]>([]);
const [isUndoable, setIsUndoable] = useState(false);
const [isRedoable, setIsRedoable] = useState(false);
-
canvasRef: Reference value for the ref to the Canvas element. -
signaturePadRef: Reference value for the SignaturePad instance. -
signatureRedoArray: An array to store history, managing the state of each stroke. -
isRedoable: A flag for the button's disabled state.
Updating button states
const updateButtonStates = useCallback(() => {
if (signaturePadRef.current) {
setIsUndoable(signaturePadRef.current.toData().length > 0);
setIsRedoable(signatureRedoArray.current.length > 0);
}
}, []);
It updates the "Undo" button state based on whether signature data exists and the "Redo" button state based on whether history exists.
Handling the input viewport
const resizeCanvas = useCallback(() => {
const canvas = canvasRef.current;
if (canvas) {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = window.innerWidth * ratio;
canvas.height = window.innerHeight * ratio;
canvas.getContext("2d")!.scale(ratio, ratio);
if (signaturePadRef.current) {
signaturePadRef.current.clear();
}
}
}, []);
This is used as a handler for the resize event to get the effective dimensions of the input screen. However, note that if the window is resized during input, the signature state will be cleared.
Initialization process
useEffect(() => {
const canvas = canvasRef.current;
if (canvas) {
const signaturePad = new SignaturePad(canvas, {
backgroundColor: 'rgb(255, 255, 255)',
});
signaturePadRef.current = signaturePad;
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
updateButtonStates();
signaturePad.addEventListener('endStroke', updateButtonStates);
return () => {
window.removeEventListener('resize', resizeCanvas);
signaturePad.removeEventListener('endStroke', updateButtonStates);
};
}
}, [resizeCanvas, updateButtonStates]);
- Instantiates
signaturePad(setting the background color). - Registers the resize event handler.
- Updates the button states every time a stroke is completed.
Implementing Undo functionality
const undo = useCallback(() => {
if (signaturePadRef.current) {
const signatureData = signaturePadRef.current.toData();
if (signatureData.length > 0) {
const lastState = signatureData.pop();
signaturePadRef.current.fromData(signatureData);
if (lastState) {
signatureRedoArray.current.push(lastState);
}
updateButtonStates();
}
}
}, [updateButtonStates]);
- Retrieves the current signature data
- Removes the last stroke
- Saves the removed stroke to the Redo array
- Updates the button states
The above steps are performed in this function.
Implementing Redo functionality
const redo = useCallback(() => {
if (signaturePadRef.current && signatureRedoArray.current.length > 0) {
const lastRedo = signatureRedoArray.current.pop();
if (lastRedo) {
const signatureData = signaturePadRef.current.toData();
signatureData.push(lastRedo);
signaturePadRef.current.fromData(signatureData);
updateButtonStates();
}
}
}, [updateButtonStates]);
- Retrieves the last stroke from the Redo array
- Adds it to the current signature data
- Updates the button states
The above steps are performed in this function.
Implementing Clear functionality
const clear = useCallback(() => {
if (signaturePadRef.current) {
signaturePadRef.current.clear();
signatureRedoArray.current = [];
setIsUndoable(false);
setIsRedoable(false);
}
}, []);
It clears the signature and resets all history and button states.
Usage Example
import { useSignature } from "./hooks/useSignature";
function SignatureComponent() {
const { canvasRef, clear, redo,
undo, isUndoable, isRedoable } = useSignature();
return (
<div>
<canvas ref={canvasRef} />
<div>
<button onClick={undo} disabled={!isUndoable}>
元に戻す
</button>
<button onClick={redo} disabled={!isRedoable}>
やり直し
</button>
<button onClick={clear}>
クリア
</button>
</div>
</div>
);
}
Summary
By turning the signature_pad library into a Hook, you can implement a signature input feature with undo/redo functionality.
Side Note
You can actually try out the screen at the link above.
Below is a screen recording from a mobile device:

Login screen (You can enter without doing anything just by pressing the login button)
↓
You can try it from "Signature Settings" (印影設定).
By the way, since this was a PWA verification prototype, if you open the URL above in a browser and save it to your home screen (or click "Install" in the search bar), it will behave like an app.
I was conducting various technical verifications to make it feel like a native app, such as testing Push notifications and enabling viewing and registration of content even when offline using indexedDB, but it's now just a memory of a project I had to give up on for "grown-up reasons" (?).
As a side note, I have also verified Push notifications as a PWA, but that feature is currently disabled.
(I'll write about that in another post lol)
*I remember being so excited when Push notifications actually worked 🤣
Discussion