n many real-world applications, capturing user signatures is crucial—especially in industries like insurance, healthcare, or field services. Whether you need sign-off on forms, approvals, or receipts, having a signature panel in your Salesforce UI can enhance your digital experience.
In this blog post, I’ll walk you through how to create a simple yet functional signature panel in LWC (Lightning Web Components) using plain JavaScript and the HTML <canvas> element.
🔧 Use Case
Allow users to draw their signature on a canvas or write it, clear it if needed, and save the captured signature as an attachment to salesforce parent record
🧱 LWC Component Structure
We’ll create the following files:
signatureCanvas.html– Template with the canvas and buttons.– JavaScript to handle drawing and interactions.signatureCanvas.js– Optional styling for better UX.signatureCanvas.css- SignatureController.apex – To save the signature
HTML:
<template>
<lightning-card title="Signature Panel">
<div class="panelclass slds-p-horizontal_small">
<lightning-input
label="Type your eSignature"
type="text"
value={name}
onchange={signIt}>
</lightning-input>
<div>
{headerText}
</div>
<div class="slds-m-bottom_small">
<canvas
name="canvasPanel"
height="200"
width="400px"
class="slds-input">
</canvas>
</div>
<div class="slds-m-bottom_small">
<lightning-button
variant="brand"
label="Download Signature"
title="Save"
onclick={downloadSignature}
class="slds-m-left_x-small">
</lightning-button>
<lightning-button
variant="brand"
label="Save Signature"
title="Save"
onclick={saveSignature}
class="slds-m-left_x-small">
</lightning-button>
<lightning-button
variant="brand"
label="Clear"
title="Clear"
onclick={handleClearClick}
class="slds-m-left_x-small">
</lightning-button>
</div>
</div>
</lightning-card>
</template>
JS:
import { LightningElement,api } from 'lwc';
import { loadStyle } from 'lightning/platformResourceLoader';
import sResource from '@salesforce/resourceUrl/signature';
import saveSignature from '@salesforce/apex/SignatureController.saveSignature';
import { ShowToastEvent } from 'lightning/platformShowToastEvent'
let isMousePressed,
isDotFlag = false,
prevX = 0,
currX = 0,
prevY = 0,
currY = 0;
let penColor = "#000000";
let lineWidth = 1.5;
let canvasElement, ctx;
let dataURL,convertedDataURI; //holds image data
export default class SignatureCanvas extends LightningElement {
@api recordId;
fileName;
@api headerText='Or Draw your eSignature using cursor!';
addEvents() {
canvasElement.addEventListener('mousemove', this.handleMouseMove.bind(this));
canvasElement.addEventListener('mousedown', this.handleMouseDown.bind(this));
canvasElement.addEventListener('mouseup', this.handleMouseUp.bind(this));
canvasElement.addEventListener('mouseout', this.handleMouseOut.bind(this));
canvasElement.addEventListener("touchstart", this.handleTouchStart.bind(this));
canvasElement.addEventListener("touchmove", this.handleTouchMove.bind(this));
canvasElement.addEventListener("touchend", this.handleTouchEnd.bind(this));
}
handleMouseMove(event){
if (isMousePressed) {
this.setupCoordinate(event);
this.redraw();
}
}
handleMouseDown(event){
event.preventDefault();
this.setupCoordinate(event);
isMousePressed = true;
isDotFlag = true;
if (isDotFlag) {
this.drawDot();
isDotFlag = false;
}
}
handleMouseUp(event){
isMousePressed = false;
}
handleMouseOut(event){
isMousePressed = false;
}
handleTouchStart(event) {
if (event.targetTouches.length == 1) {
this.setupCoordinate(event);
}
};
handleTouchMove(event) {
// Prevent scrolling.
event.preventDefault();
this.setupCoordinate(event);
this.redraw();
};
handleTouchEnd(event) {
var wasCanvasTouched = event.target === canvasElement;
if (wasCanvasTouched) {
event.preventDefault();
this.setupCoordinate(event);
this.redraw();
}
};
renderedCallback() {
canvasElement = this.template.querySelector('canvas');
ctx = canvasElement.getContext("2d");
ctx.lineCap = 'round';
this.addEvents();
}
signIt(e){
var signText = e.detail.value;
this.fileName=signText;
ctx.font = "30px GreatVibes-Regular";
this.handleClearClick(e);
ctx.fillText(signText, 30, canvasElement.height/2);
}
downloadSignature(e) {
dataURL = canvasElement.toDataURL("image/jpg");
this.downloadSign(e);
}
saveSignature(e) {
dataURL = canvasElement.toDataURL("image/jpg");
//convert that as base64 encoding
convertedDataURI = dataURL.replace(/^data:image\/(png|jpg);base64,/, "");
saveSignature({signElement: convertedDataURI,recId : this.recordId})
.then(result => {
this.dispatchEvent(
new ShowToastEvent({
title: 'Success',
message: 'Successfully captured your signature!',
variant: 'success',
}),
);
})
.catch(error => {
//show error message
this.dispatchEvent(
new ShowToastEvent({
title: 'Sorry, unable to capture your signature',
message: error.body.message,
variant: 'error',
}),
);
});
}
downloadSign(e){
var link = document.createElement('a');
link.download = '.jpg';
link.href = dataURL;
link.click();
}
handleClearClick(){
ctx.clearRect(0, 0, canvasElement.width, canvasElement.height);
}
setupCoordinate(eventParam){
const clientRect = canvasElement.getBoundingClientRect();
prevX = currX;
prevY = currY;
currX = eventParam.clientX - clientRect.left;
currY = eventParam.clientY - clientRect.top;
}
redraw() {
ctx.beginPath();
ctx.moveTo(prevX, prevY);
ctx.lineTo(currX, currY);
ctx.strokeStyle = penColor;
ctx.lineWidth = lineWidth;
ctx.closePath();
ctx.stroke();
}
drawDot(){
ctx.beginPath();
ctx.fillStyle = penColor;
ctx.fillRect(currX, currY, lineWidth, lineWidth);
ctx.closePath();
}
}
CSS:
@font-face {
font-family: 'GreatVibes-Regular';
src: url('../../resource/signature/GreatVibes-Regular.ttf');
font-weight: normal;
font-style: normal;
}
.canvasclass {
border:1px solid rgb(0, 0, 0);
background: transparent;
}
.panelclass {
width: 400px;
}
META:
<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>57.0</apiVersion>
<isExposed>true</isExposed>
<masterLabel>Signature Canvas</masterLabel>
<targets>
<target>lightningCommunity__Page</target>
<target>lightningCommunity__Default</target>
<target>lightning__RecordPage</target>
<target>lightning__AppPage</target>
<target>lightning__HomePage</target>
<target>lightning__FlowScreen</target>
</targets>
</LightningComponentBundle>
Apex Controller:
public with sharing class SignatureController {
@AuraEnabled
public static void saveSignature(String signElement,Id recId){
ContentVersion cVersion = new ContentVersion();
cVersion.ContentLocation = 'S'; //S-Document is in Salesforce.
cVersion.PathOnClient = 'Signature-'+System.now() +'.png';//File name with extention
cVersion.Origin = 'H';//C-Content Origin. H-Chatter Origin.
cVersion.Title = 'Signature-'+recId+'-'+System.now() +'.png';//Name of the file
cVersion.VersionData = EncodingUtil.base64Decode(signElement);//File content
Insert cVersion;
//Get the Uploaded doc ContentDocumentId
Id conDocument = [SELECT ContentDocumentId FROM ContentVersion WHERE Id =:cVersion.Id].ContentDocumentId;
//Insert ContentDocumentLink to share
ContentDocumentLink cDocLink = new ContentDocumentLink();
cDocLink.ContentDocumentId = conDocument;
cDocLink.LinkedEntityId = recId;
cDocLink.ShareType = 'I';
cDocLink.Visibility = 'AllUsers';
Insert cDocLink;
}
}
✅ Final Thoughts
Creating a signature panel in LWC is easier than you might think. With just a few lines of JavaScript and HTML canvas, you can offer a smooth, intuitive signature experience inside Salesforce.
If you found this useful, feel free to share or drop a comment. Happy coding! ✨
😎 Preview


