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:
- LWC Accessibility Guidelines - WCAG 2.2 compliance guidance
- LWC Accessibility Examples - Complete code examples
- LWC Accessibility Quick Start - Quick start guide
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
- ✅ All form controls have
labelattributes - ✅ Error messages use
role="alert"andaria-describedby - ✅ Autocomplete attributes for personal information
- ✅ Fieldset/legend for grouped form controls
- ✅ Required field indicators
Keyboard Navigation
- ✅ All interactive elements are keyboard accessible
- ✅ Focus indicators are visible (2px outline, 3:1 contrast)
- ✅ Tab order is logical
- ✅ Modals trap focus and close on Escape key
- ✅ Focus returns to trigger element after modal close
- ✅ Space and Enter keys work for custom buttons
ARIA Attributes
- ✅ Icon buttons have
aria-label - ✅ Custom components have appropriate
role - ✅ ARIA states used (aria-busy, aria-checked, etc.)
- ✅ ARIA live regions for dynamic content
- ✅ Modal dialogs have
role="dialog"andaria-modal="true"
Images
- ✅ Icons have
alternative-textattributes - ✅ Decorative elements use
aria-hidden="true"
Semantic HTML
- ✅ Proper heading hierarchy (h1 → h2)
- ✅ Semantic regions (header, main, article, section)
- ✅ Proper list markup where applicable
Color and Contrast
- ✅ Uses SLDS color tokens for proper contrast
- ✅ Focus indicators meet 3:1 contrast ratio
- ✅ Error text meets 4.5:1 contrast ratio
Dynamic Content
- ✅ Loading states announced with
aria-live="polite"andaria-busy - ✅ Error messages announced with
role="alert"andaria-live="assertive" - ✅ Success messages announced with
role="status"
Usage Instructions
- Copy Template Files: Copy all four files to your component directory
- Rename Component: Replace
accessibleComponentwith your component name - Customize Content: Update template content for your use case
- Add Properties: Add
@apiproperties as needed - Implement Logic: Add your business logic in JavaScript
- Test Accessibility: Run accessibility tests (axe-core, Lighthouse)
- Test with Screen Reader: Test with NVDA, JAWS, or VoiceOver
Testing Checklist
- All form controls have labels
- All interactive elements are keyboard accessible
- Focus indicators are visible
- ARIA attributes are correct
- Color contrast meets WCAG standards
- Dynamic content is announced
- Modals trap focus
- Error messages are accessible
- Tested with screen reader
- Tested with keyboard-only navigation
Related Patterns
- LWC Accessibility Guidelines - WCAG 2.2 compliance guidance
- LWC Accessibility Examples - Complete code examples
- LWC Accessibility Quick Start - Quick start guide
- LWC Accessibility Testing - Testing patterns