Skip to main content

How to Implement Custom Form Validation in Salesforce LWC (setCustomValidity & reportValidity)

In plain words: Custom validation in Lightning Web Components (LWC) allows developers to check user inputs against complex business rules before submission by injecting custom error messages with setCustomValidity() and rendering the native Salesforce red error banner using reportValidity().

While standard attributes like required, minlength, and pattern handle basic constraints, real-world business forms often require dynamic, cross-field validation rules (such as validating email domain formats, custom password criteria, or comparing date ranges). Implementing platform-compliant client-side validation stops bad data before it hits Apex or database triggers.

Prerequisites

  • A Salesforce Developer Edition org, Scratch Org, or Sandbox.
  • Salesforce CLI (sf) installed and authenticated.
  • Familiarity with JavaScript DOM querying and standard Lightning base components.

Step 1: Set Up the Component Structure

Generate the LWC bundle via Salesforce CLI:
sf lightning generate component -n customValidationForm -d force-app/main/default/lwc --type lwc

Step 2: Build the Form HTML Template

In customValidationForm.html, place standard lightning-input elements and bind them with descriptive data-field identifiers:

<template>
    <lightning-card title="Custom Form Validation Demo" icon-name="utility:form">
        <div class="slds-p-around_medium">
            
            <div class="slds-grid slds-gutters slds-wrap">
                <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-m-bottom_small">
                    <lightning-input 
                        label="Username" 
                        data-field="username" 
                        value={formData.username} 
                        onchange={handleInputChange} 
                        placeholder="At least 5 alphanumeric characters"
                        required>
                    </lightning-input>
                </div>

                <div class="slds-col slds-size_1-of-1 slds-medium-size_1-of-2 slds-m-bottom_small">
                    <lightning-input 
                        type="email" 
                        label="Corporate Email" 
                        data-field="email" 
                        value={formData.email} 
                        onchange={handleInputChange} 
                        placeholder="user@company.com"
                        required>
                    </lightning-input>
                </div>
            </div>

            <!-- Action Buttons -->
            <div class="slds-m-top_medium slds-button-group" role="group">
                <lightning-button 
                    variant="brand" 
                    label="Submit Registration" 
                    onclick={handleSubmit}>
                </lightning-button>
                <lightning-button 
                    variant="neutral" 
                    label="Reset Form" 
                    onclick={handleReset}>
                </lightning-button>
            </div>

        </div>
    </lightning-card>
</template>

Step 3: Implement Dynamic Custom Validation in JavaScript

In customValidationForm.js, query input elements, execute regex/business rules, set custom error strings, and trigger visual indicators via reportValidity():

import { LightningElement, track } from 'lwc';
import { ShowToastEvent } from 'lightning/platformShowToastEvent';

const USERNAME_REGEX = /^[a-zA-Z0-9_]{5,15}$/;
const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;

export default class CustomValidationForm extends LightningElement {
    @track formData = {
        username: '',
        email: ''
    };

    handleInputChange(event) {
        const field = event.target.dataset.field;
        this.formData[field] = event.target.value.trim();

        // Clear custom error as user types to prevent sticky validation blocks
        event.target.setCustomValidity('');
        event.target.reportValidity();
    }

    validateAllFields() {
        let isAllValid = true;

        const usernameInput = this.template.querySelector('[data-field="username"]');
        const emailInput = this.template.querySelector('[data-field="email"]');

        // 1. Custom Username Validation
        if (!this.formData.username) {
            usernameInput.setCustomValidity('Username is required.');
            isAllValid = false;
        } else if (!USERNAME_REGEX.test(this.formData.username)) {
            usernameInput.setCustomValidity(
                'Username must be 5-15 characters and contain only letters, numbers, or underscores.'
            );
            isAllValid = false;
        } else {
            usernameInput.setCustomValidity('');
        }
        usernameInput.reportValidity();

        // 2. Custom Email Validation
        if (!this.formData.email) {
            emailInput.setCustomValidity('Corporate email is required.');
            isAllValid = false;
        } else if (!EMAIL_REGEX.test(this.formData.email)) {
            emailInput.setCustomValidity('Please enter a valid corporate email format (e.g. name@company.com).');
            isAllValid = false;
        } else if (this.formData.email.endsWith('@example.com')) {
            emailInput.setCustomValidity('Public test domains like @example.com are not permitted.');
            isAllValid = false;
        } else {
            emailInput.setCustomValidity('');
        }
        emailInput.reportValidity();

        return isAllValid;
    }

    handleSubmit() {
        const isValid = this.validateAllFields();

        if (isValid) {
            // Form is clean - execute Apex mutation or LDS createRecord here
            this.showToast('Validation Success', 'Form passed all custom business rules!', 'success');
        } else {
            this.showToast('Validation Error', 'Please resolve the highlighted errors before submitting.', 'error');
        }
    }

    handleReset() {
        this.formData = { username: '', email: '' };

        const allInputs = this.template.querySelectorAll('lightning-input');
        allInputs.forEach(input => {
            input.value = '';
            input.setCustomValidity('');
            input.reportValidity();
        });

        this.showToast('Reset Complete', 'Form inputs have been cleared.', 'info');
    }

    showToast(title, message, variant) {
        this.dispatchEvent(new ShowToastEvent({ title, message, variant }));
    }
}
Warning Trap: Calling setCustomValidity('Error message') sets the custom error on the component's internal validity state, but does not render the error message on the UI until you call reportValidity(). Furthermore, when the input becomes valid, you must explicitly pass an empty string setCustomValidity(''); otherwise, the field will remain permanently invalid.
360 Architecture Summary:
  • setCustomValidity(msg): Injects custom error strings into the component validity state. Pass '' to clear errors.
  • reportValidity(): Evaluates the element's validity state and draws or clears red SLDS error text on the screen.
  • checkValidity(): Returns a boolean (true or false) without changing the UI.
  • Multi-Field Validation: Use Array.reduce() across querySelectorAll to evaluate entire forms in a single statement.

Step 4: Configure Metadata and Deploy

Update customValidationForm.js-meta.xml to expose the component in Lightning App Builder:

<?xml version="1.0" encoding="UTF-8"?>
<LightningComponentBundle xmlns="http://soap.sforce.com/2006/04/metadata">
    <apiVersion>60.0</apiVersion>
    <isExposed>true</isExposed>
    <targets>
        <target>lightning__AppPage</target>
        <target>lightning__RecordPage</target>
        <target>lightning__HomePage</target>
    </targets>
</LightningComponentBundle>
Deployment & Testing:
# Deploy to default org
sf project deploy start

# Open target org
sf org open
  • In Salesforce Setup, navigate to Lightning App Builder.
  • Add customValidationForm to a Record or App page, then save and activate.
  • Enter invalid inputs (e.g. short usernames or test@example.com) to verify custom error messaging.
Core Takeaway: Combining setCustomValidity() with reportValidity() provides native, accessible, and user-friendly client-side validation before sending data to server-side Apex controllers.