LWC Accessibility Quick Start Guide

Getting started with Lightning Web Component accessibility in Salesforce.

Overview

This quick-start guide provides step-by-step instructions for making Lightning Web Components accessible, following WCAG 2.2 standards and Salesforce best practices.

Related Patterns:

Quick Accessibility Checklist

Use this checklist when building or reviewing LWC components:

Form Accessibility

Keyboard Navigation

ARIA Attributes

Images

Semantic HTML

Color and Contrast


Step-by-Step: Making an Existing Component Accessible

Step 1: Add Form Labels

Before:

<lightning-input
    name="email"
    value={email}
    onchange={handleChange}>
</lightning-input>

After:

<lightning-input
    label="Email Address"
    name="email"
    value={email}
    onchange={handleChange}
    required
    message-when-value-missing="Email is required">
</lightning-input>

Step 2: Add Keyboard Support

Before:

<div class="custom-button" onclick={handleClick}>
    Click me
</div>

After:

<div
    class="custom-button"
    role="button"
    aria-label="Click to submit form"
    onclick={handleClick}
    onkeydown={handleKeyDown}
    tabindex="0">
    Click me
</div>

JavaScript:

handleKeyDown(event) {
    if (event.key === ' ' || event.key === 'Enter') {
        event.preventDefault();
        this.handleClick();
    }
}

Step 3: Add ARIA Labels to Icon Buttons

Before:

<button class="slds-button slds-button_icon" onclick={handleClose}>
    <lightning-icon icon-name="utility:close"></lightning-icon>
</button>

After:

<button
    class="slds-button slds-button_icon"
    onclick={handleClose}
    aria-label="Close dialog"
    title="Close">
    <lightning-icon
        icon-name="utility:close"
        alternative-text="Close">
    </lightning-icon>
    <span class="slds-assistive-text">Close</span>
</button>

Step 4: Add Focus Indicators

Before (CSS):

.custom-button {
    border: none;
    outline: none; /* Removes focus indicator */
}

After (CSS):

.custom-button {
    border: none;
}

.custom-button:focus {
    outline: 2px solid #0176d3;
    outline-offset: 2px;
}

.custom-button:focus-visible {
    outline: 2px solid #0176d3;
    outline-offset: 2px;
}

Step 5: Add Error Message Accessibility

Before:

<div class="error" if:true={errorMessage}>
    {errorMessage}
</div>

After:

<div
    role="alert"
    aria-live="assertive"
    class="error"
    if:true={errorMessage}>
    {errorMessage}
</div>

Step 6: Fix Image Alt Text

Before:

<img src="/assets/chart.png" class="chart">

After (Informative):

<img
    src="/assets/chart.png"
    alt="Sales chart showing 25% increase in Q4 2023"
    class="chart">

After (Decorative):

<img
    src="/assets/divider.png"
    alt=""
    class="divider"
    aria-hidden="true">

Step 7: Fix Heading Hierarchy

Before:

<h1>Page Title</h1>
<h3>Section Title</h3> <!-- Should be h2 -->
<h4>Subsection</h4> <!-- Should be h3 -->

After:

<h1>Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection</h3>

Step 8: Add Semantic HTML

Before:

<div class="header">...</div>
<div class="main">...</div>
<div class="footer">...</div>

After:

<header>...</header>
<main>...</main>
<footer>...</footer>

Essential Patterns

Pattern 1: Accessible Form

Complete Example:

<template>
    <lightning-card title="Contact Form">
        <div class="slds-p-around_medium">
            <form onsubmit={handleSubmit}>
                <div class="slds-form-element">
                    <lightning-input
                        label="First Name"
                        name="firstName"
                        value={firstName}
                        onchange={handleInputChange}
                        required
                        autocomplete="given-name"
                        aria-describedby="firstName-help">
                    </lightning-input>
                    <div id="firstName-help" class="slds-form-element__help">
                        Enter your legal first name
                    </div>
                </div>

                <div class="slds-form-element">
                    <lightning-input
                        type="email"
                        label="Email"
                        name="email"
                        value={email}
                        onchange={handleInputChange}
                        required
                        autocomplete="email"
                        aria-describedby="email-error"
                        message-when-value-missing="Email is required">
                    </lightning-input>
                    <div
                        id="email-error"
                        class="slds-form-element__help"
                        role="alert"
                        if:true={emailError}>
                        {emailError}
                    </div>
                </div>

                <div class="slds-m-top_medium">
                    <lightning-button
                        type="submit"
                        label="Submit"
                        variant="brand">
                    </lightning-button>
                </div>
            </form>
        </div>
    </lightning-card>
</template>

Pattern 2: Accessible Button

Complete Example:

<template>
    <button
        type="button"
        class="slds-button slds-button_brand"
        aria-label={ariaLabel}
        onclick={handleClick}
        onkeydown={handleKeyDown}>
        <lightning-icon
            if:true={iconName}
            icon-name={iconName}
            size="small"
            alternative-text={iconAltText}>
        </lightning-icon>
        <span if:true={label}>{label}</span>
        <span class="slds-assistive-text" if:true={assistiveText}>
            {assistiveText}
        </span>
    </button>
</template>

JavaScript:

import { LightningElement, api } from 'lwc';

export default class AccessibleButton extends LightningElement {
    @api label = '';
    @api iconName = '';
    @api ariaLabel = '';
    @api assistiveText = '';

    get iconAltText() {
        return this.ariaLabel || this.label;
    }

    handleKeyDown(event) {
        if (event.key === ' ' || event.key === 'Enter') {
            event.preventDefault();
            this.handleClick();
        }
    }

    handleClick() {
        this.dispatchEvent(new CustomEvent('click'));
    }
}

Pattern 3: Accessible Modal

Complete Example:

<template>
    <template if:true={isOpen}>
        <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">
                        {title}
                    </h2>
                    <button
                        class="slds-button slds-button_icon slds-modal__close"
                        aria-label="Close {title}"
                        onclick={handleClose}>
                        <lightning-icon icon-name="utility:close" size="small"></lightning-icon>
                        <span class="slds-assistive-text">Close</span>
                    </button>
                </header>
                <div class="slds-modal__content" id="modal-description">
                    <slot></slot>
                </div>
                <footer class="slds-modal__footer">
                    <lightning-button
                        label="Cancel"
                        variant="neutral"
                        onclick={handleClose}>
                    </lightning-button>
                    <lightning-button
                        label="Confirm"
                        variant="brand"
                        onclick={handleConfirm}>
                    </lightning-button>
                </footer>
            </div>
        </section>
        <div class="slds-backdrop slds-backdrop_open" role="presentation"></div>
    </template>
</template>

JavaScript (focus trapping):

import { LightningElement, api } from 'lwc';

export default class AccessibleModal extends LightningElement {
    @api title = 'Modal Title';
    @api isOpen = false;
    
    previousActiveElement = null;

    renderedCallback() {
        if (this.isOpen) {
            this.previousActiveElement = document.activeElement;
            this.trapFocus();
        }
    }

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

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

        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();
                    }
                }
            }
            if (e.key === 'Escape') {
                this.handleClose();
            }
        });
    }

    handleClose() {
        this.isOpen = false;
        if (this.previousActiveElement) {
            this.previousActiveElement.focus();
        }
        this.dispatchEvent(new CustomEvent('close'));
    }

    handleConfirm() {
        this.dispatchEvent(new CustomEvent('confirm'));
        this.handleClose();
    }
}

Pattern 4: Accessible Data Table

Complete Example:

<template>
    <table
        class="slds-table slds-table_cell-buffer"
        role="table"
        aria-label="Contact list with {contactCount} contacts">
        <thead>
            <tr>
                <th scope="col">Name</th>
                <th scope="col">Email</th>
                <th scope="col">Phone</th>
            </tr>
        </thead>
        <tbody>
            <template for:each={contacts} for:item="contact">
                <tr key={contact.Id}>
                    <td data-label="Name">{contact.Name}</td>
                    <td data-label="Email">
                        <a href="mailto:{contact.Email}" aria-label="Email {contact.Name}">
                            {contact.Email}
                        </a>
                    </td>
                    <td data-label="Phone">{contact.Phone}</td>
                </tr>
            </template>
        </tbody>
    </table>
</template>

Testing Your Component

Quick Test Checklist

  1. Keyboard Navigation:
    • Tab through all interactive elements
    • Focus indicators are visible
    • Enter/Space activate buttons
    • Escape closes modals
  2. Screen Reader (NVDA, JAWS, or VoiceOver):
    • All form labels are announced
    • Error messages are announced
    • Button purposes are clear
    • Images have appropriate alt text
  3. Automated Testing:
    • Run axe-core scan (no violations)
    • Run Lighthouse accessibility audit (90+ score)
    • Run Jest accessibility tests

Testing Tools


Common Fixes

Fix 1: Missing Label

<!-- Add label attribute -->
<lightning-input label="Email" ...>

Fix 2: Missing ARIA Label

<!-- Add aria-label -->
<button aria-label="Close dialog" ...>

Fix 3: Missing Focus Indicator

/* Add focus styles */
.button:focus {
    outline: 2px solid #0176d3;
    outline-offset: 2px;
}

Fix 4: Missing Alt Text

<!-- Add alt text -->
<img src="chart.png" alt="Sales chart showing Q4 results">

Fix 5: Incorrect Heading Hierarchy

<!-- Fix hierarchy -->
<h1>Title</h1>
<h2>Section</h2> <!-- Not h3 -->

Next Steps

  1. Review Examples: See LWC Accessibility Examples for complete code examples
  2. Learn Guidelines: Read LWC Accessibility Guidelines for WCAG 2.2 compliance
  3. Test Your Components: Follow LWC Accessibility Testing patterns
  4. Fix Issues: Use LWC Accessibility Troubleshooting for common errors