Accessible LWC Component Template

Template for creating accessible Lightning Web Components following WCAG 2.2 standards.

Overview

This template provides a complete, accessible LWC component structure with all accessibility best practices built in. Use this as a starting point for new components.

Related Patterns:

Component Structure

File: accessibleComponent.html

<template>
    <article>
        <header>
            <h1>{title}</h1>
            <p class="slds-text-body_small" if:true={subtitle}>{subtitle}</p>
        </header>

        <main>
            <!-- Loading State -->
            <div
                role="status"
                aria-live="polite"
                aria-busy={isLoading}
                class="slds-m-bottom_medium"
                if:true={isLoading}>
                <lightning-spinner
                    alternative-text="Loading {title}"
                    size="medium">
                </lightning-spinner>
                <span class="slds-assistive-text">Loading {title}, please wait</span>
            </div>

            <!-- Error State -->
            <div
                role="alert"
                aria-live="assertive"
                class="slds-text-color_error slds-m-bottom_medium"
                if:true={errorMessage}>
                <lightning-icon
                    icon-name="utility:error"
                    size="small"
                    alternative-text="Error">
                </lightning-icon>
                {errorMessage}
            </div>

            <!-- Main Content -->
            <div if:false={isLoading} if:false={errorMessage}>
                <!-- Form Example -->
                <form onsubmit={handleSubmit} if:true={showForm}>
                    <fieldset class="slds-form-element">
                        <legend class="slds-form-element__legend slds-form-element__label">
                            {formGroupLabel}
                        </legend>

                        <div class="slds-form-element">
                            <lightning-input
                                label="Field Label"
                                name="fieldName"
                                value={fieldValue}
                                onchange={handleInputChange}
                                required
                                autocomplete="off"
                                aria-describedby="field-help"
                                message-when-value-missing="This field is required">
                            </lightning-input>
                            <div id="field-help" class="slds-form-element__help">
                                Help text for this field
                            </div>
                            <div
                                id="field-error"
                                class="slds-form-element__help slds-text-color_error"
                                role="alert"
                                if:true={fieldError}>
                                {fieldError}
                            </div>
                        </div>
                    </fieldset>

                    <div class="slds-m-top_medium">
                        <lightning-button
                            type="submit"
                            label="Submit"
                            variant="brand"
                            onclick={handleSubmit}>
                        </lightning-button>
                        <lightning-button
                            label="Cancel"
                            variant="neutral"
                            onclick={handleCancel}>
                        </lightning-button>
                    </div>
                </form>

                <!-- Data Display Example -->
                <div if:true={showData}>
                    <section aria-labelledby="data-heading">
                        <h2 id="data-heading">Data Display</h2>
                        <div
                            role="status"
                            aria-live="polite"
                            if:true={dataUpdated}>
                            Data updated successfully
                        </div>
                        <!-- Data content here -->
                    </section>
                </div>

                <!-- Interactive Elements Example -->
                <div class="slds-m-top_medium">
                    <button
                        type="button"
                        class="slds-button slds-button_brand"
                        aria-label={buttonAriaLabel}
                        onclick={handleAction}
                        onkeydown={handleKeyDown}>
                        <lightning-icon
                            if:true={iconName}
                            icon-name={iconName}
                            size="small"
                            alternative-text={iconAltText}>
                        </lightning-icon>
                        <span if:true={buttonLabel}>{buttonLabel}</span>
                        <span class="slds-assistive-text" if:true={assistiveText}>
                            {assistiveText}
                        </span>
                    </button>
                </div>
            </div>
        </main>

        <!-- Modal Example -->
        <template if:true={showModal}>
            <section
                role="dialog"
                aria-modal="true"
                aria-labelledby="modal-title"
                aria-describedby="modal-description"
                class="slds-modal slds-fade-in-open"
                tabindex="-1">
                <div class="slds-modal__container">
                    <header class="slds-modal__header">
                        <h2 id="modal-title" class="slds-modal__title">
                            {modalTitle}
                        </h2>
                        <button
                            class="slds-button slds-button_icon slds-modal__close"
                            aria-label="Close {modalTitle}"
                            onclick={handleCloseModal}
                            onkeydown={handleModalKeyDown}>
                            <lightning-icon
                                icon-name="utility:close"
                                size="small"
                                alternative-text="Close">
                            </lightning-icon>
                            <span class="slds-assistive-text">Close</span>
                        </button>
                    </header>
                    <div class="slds-modal__content" id="modal-description">
                        <slot name="modal-content"></slot>
                    </div>
                    <footer class="slds-modal__footer">
                        <lightning-button
                            label="Cancel"
                            variant="neutral"
                            onclick={handleCloseModal}>
                        </lightning-button>
                        <lightning-button
                            label="Confirm"
                            variant="brand"
                            onclick={handleConfirmModal}>
                        </lightning-button>
                    </footer>
                </div>
            </section>
            <div class="slds-backdrop slds-backdrop_open" role="presentation"></div>
        </template>
    </article>
</template>

File: accessibleComponent.js

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

export default class AccessibleComponent extends LightningElement {
    // Public properties
    @api title = 'Component Title';
    @api subtitle = '';
    @api showForm = false;
    @api showData = false;
    @api showModal = false;
    @api modalTitle = 'Modal Title';
    @api buttonLabel = 'Action';
    @api buttonAriaLabel = 'Perform action';
    @api iconName = '';
    @api assistiveText = '';

    // Tracked properties
    @track fieldValue = '';
    @track fieldError = '';
    @track isLoading = false;
    @track errorMessage = '';
    @track dataUpdated = false;

    // Computed properties
    get iconAltText() {
        return this.buttonAriaLabel || this.buttonLabel;
    }

    get formGroupLabel() {
        return 'Form Group';
    }

    // Lifecycle hooks
    connectedCallback() {
        // Component initialization
    }

    renderedCallback() {
        // Handle focus trapping for modal
        if (this.showModal) {
            this.trapFocus();
        }
    }

    // Event handlers
    handleInputChange(event) {
        const field = event.target.name;
        this[field] = event.target.value;
        
        // Clear error when user starts typing
        if (field === 'fieldName') {
            this.fieldError = '';
        }
    }

    handleSubmit(event) {
        event.preventDefault();
        
        // Validate form
        if (!this.validateForm()) {
            return;
        }

        // Submit form
        this.isLoading = true;
        // Perform async operation
        this.processForm()
            .then(() => {
                this.isLoading = false;
                this.showSuccessMessage('Form submitted successfully');
            })
            .catch((error) => {
                this.isLoading = false;
                this.errorMessage = 'Failed to submit form: ' + error.message;
            });
    }

    handleCancel() {
        // Reset form
        this.fieldValue = '';
        this.fieldError = '';
    }

    handleAction() {
        // Perform action
        this.dispatchEvent(new CustomEvent('action'));
    }

    handleKeyDown(event) {
        // Support Space and Enter keys for custom buttons
        if (event.key === ' ' || event.key === 'Enter') {
            event.preventDefault();
            this.handleAction();
        }
    }

    handleCloseModal() {
        this.showModal = false;
        // Return focus to trigger element
        if (this.previousActiveElement) {
            this.previousActiveElement.focus();
        }
        this.dispatchEvent(new CustomEvent('close'));
    }

    handleConfirmModal() {
        this.dispatchEvent(new CustomEvent('confirm'));
        this.handleCloseModal();
    }

    handleModalKeyDown(event) {
        // Close modal on Escape key
        if (event.key === 'Escape') {
            this.handleCloseModal();
        }
    }

    // Validation
    validateForm() {
        if (!this.fieldValue) {
            this.fieldError = 'This field is required';
            // Focus on field for screen reader users
            const field = this.template.querySelector('[name="fieldName"]');
            if (field) {
                field.focus();
            }
            return false;
        }
        return true;
    }

    // Focus management
    trapFocus() {
        const modal = this.template.querySelector('[role="dialog"]');
        if (!modal) return;

        // Store previous active element
        this.previousActiveElement = document.activeElement;

        // Focus on modal
        modal.focus();

        const focusableElements = modal.querySelectorAll(
            'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
        );
        const firstElement = focusableElements[0];
        const lastElement = focusableElements[focusableElements.length - 1];

        // Trap focus within modal
        modal.addEventListener('keydown', (e) => {
            if (e.key === 'Tab') {
                if (e.shiftKey) {
                    if (document.activeElement === firstElement) {
                        e.preventDefault();
                        lastElement.focus();
                    }
                } else {
                    if (document.activeElement === lastElement) {
                        e.preventDefault();
                        firstElement.focus();
                    }
                }
            }
        });
    }

    // Utility methods
    async processForm() {
        // Simulate async operation
        return new Promise((resolve) => {
            setTimeout(() => {
                resolve();
            }, 1000);
        });
    }

    showSuccessMessage(message) {
        this.dispatchEvent(
            new ShowToastEvent({
                title: 'Success',
                message: message,
                variant: 'success'
            })
        );
        this.dataUpdated = true;
        // Clear success message after 3 seconds
        setTimeout(() => {
            this.dataUpdated = false;
        }, 3000);
    }
}

File: accessibleComponent.css

/* Focus indicators */
button:focus,
input:focus,
select:focus,
textarea:focus,
[tabindex="0"]:focus {
    outline: 2px solid #0176d3;
    outline-offset: 2px;
}

button:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible,
[tabindex="0"]:focus-visible {
    outline: 2px solid #0176d3;
    outline-offset: 2px;
}

/* Ensure focus is visible on all backgrounds */
.slds-button:focus {
    box-shadow: 0 0 0 2px #0176d3;
}

/* High contrast for error states */
.slds-text-color_error {
    color: #c23934; /* Meets 4.5:1 contrast ratio */
}

/* Loading state styles */
[aria-busy="true"] {
    opacity: 0.6;
}

/* Modal backdrop */
.slds-backdrop {
    background-color: rgba(0, 0, 0, 0.6);
}

File: accessibleComponent.js-meta.xml

<?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>
    <targetConfigs>
        <targetConfig targets="lightning__AppPage,lightning__RecordPage,lightning__HomePage">
            <property name="title" type="String" label="Title" default="Component Title"/>
            <property name="subtitle" type="String" label="Subtitle"/>
            <property name="showForm" type="Boolean" label="Show Form" default="false"/>
            <property name="showData" type="Boolean" label="Show Data" default="false"/>
        </targetConfig>
    </targetConfigs>
</LightningComponentBundle>

Accessibility Features Included

Form Accessibility

Keyboard Navigation

ARIA Attributes

Images

Semantic HTML

Color and Contrast

Dynamic Content

Usage Instructions

  1. Copy Template Files: Copy all four files to your component directory
  2. Rename Component: Replace accessibleComponent with your component name
  3. Customize Content: Update template content for your use case
  4. Add Properties: Add @api properties as needed
  5. Implement Logic: Add your business logic in JavaScript
  6. Test Accessibility: Run accessibility tests (axe-core, Lighthouse)
  7. Test with Screen Reader: Test with NVDA, JAWS, or VoiceOver

Testing Checklist