How to Create a Signature Panel Using JavaScript in LWC – Salesforce

Published by

on


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.
  • signatureCanvas.js – JavaScript to handle drawing and interactions.
  • signatureCanvas.css – Optional styling for better UX.
  • 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