LWC Accessibility Errors and Solutions

Troubleshooting guide for common Lightning Web Component accessibility errors with solutions and prevention strategies.

Overview

This guide provides solutions for common LWC accessibility errors encountered during Salesforce development, including error messages, causes, solutions, and prevention strategies. All solutions follow WCAG 2.2 standards.

Related Patterns:

Prerequisites

Required Knowledge:

Recommended Reading:

Common Accessibility Errors

Error 1: Missing Form Labels

Error Message: “Form control has no associated label” (axe-core, Lighthouse)

Common Causes:

Solutions:

Solution 1: Use Lightning Base Component Label Attribute

Before: Missing label

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

After: With label

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

Solution 2: Use Native Label Element

Before: Custom input without label

<input
    type="text"
    name="customField"
    value={customValue}
    onchange={handleChange}>

After: With label

<label for="custom-field">
    Custom Field
    <input
        type="text"
        id="custom-field"
        name="customField"
        value={customValue}
        onchange={handleChange}>
</label>

Solution 3: Use ARIA Label When Visual Label Not Feasible

Before: Icon-only input

<div class="search-container">
    <lightning-icon icon-name="utility:search"></lightning-icon>
    <input type="search" name="search">
</div>

After: With ARIA label

<div class="search-container">
    <lightning-icon icon-name="utility:search" alternative-text="Search"></lightning-icon>
    <input
        type="search"
        name="search"
        aria-label="Search contacts"
        aria-describedby="search-help">
    <span id="search-help" class="slds-assistive-text">Enter contact name or email</span>
</div>

Prevention:


Error 2: Missing ARIA Labels

Error Message: “Element has no accessible name” (axe-core, Lighthouse)

Common Causes:

Solutions:

Solution 1: Add ARIA Label to Icon Buttons

Before: Icon button without label

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

After: With ARIA label

<button
    class="slds-button slds-button_icon"
    onclick={handleClick}
    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>

Solution 2: Add ARIA Label to Custom Components

Before: Custom toggle without label

<div class="toggle" onclick={handleToggle} tabindex="0">
    <span class="toggle-slider"></span>
</div>

After: With ARIA label and role

<div
    class="toggle"
    role="switch"
    aria-label="Enable notifications"
    aria-checked={isEnabled}
    onclick={handleToggle}
    onkeydown={handleKeyDown}
    tabindex="0">
    <span class="toggle-slider"></span>
</div>

Prevention:


Error 3: Keyboard Traps

Error Message: “Focus is trapped” (manual testing, axe-core)

Common Causes:

Solutions:

Solution 1: Implement Focus Trapping in Modal

Before: Modal without focus trap

handleOpen() {
    this.isOpen = true;
}

After: With focus trap

handleOpen() {
    this.isOpen = true;
    this.previousActiveElement = document.activeElement;
    this.trapFocus();
}

trapFocus() {
    const modal = this.template.querySelector('[role="dialog"]');
    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();
    }
}

Prevention:


Error 4: Missing Focus Indicators

Error Message: “Focus indicator not visible” (manual testing, Lighthouse)

Common Causes:

Solutions:

Solution 1: Add Visible Focus Styles

Before: No focus indicator

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

After: With focus indicator

.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;
}

Solution 2: Use SLDS Focus Styles

Before: Custom focus styles

.my-component:focus {
    outline: 1px solid gray; /* Low contrast */
}

After: Using SLDS tokens

.my-component:focus {
    outline: 2px solid var(--slds-g-color-brand-base-60, #0176d3);
    outline-offset: 2px;
}

Prevention:


Error 5: Insufficient Color Contrast

Error Message: “Text has insufficient color contrast” (Lighthouse, axe-core)

Common Causes:

Solutions:

Solution 1: Use SLDS Color Tokens

Before: Custom colors with low contrast

.error-text {
    color: #ff0000; /* Red on white: 4.0:1 - fails */
}

After: Using SLDS tokens

.error-text {
    color: var(--slds-g-color-error-base-10, #c23934); /* Meets 4.5:1 */
}

Solution 2: Adjust Colors to Meet Contrast

Before: Low contrast text

<div style="color: #999999; background: #ffffff;">
    Low contrast text
</div>

After: High contrast text

<div style="color: #3e3e3c; background: #ffffff;">
    High contrast text (7.1:1 ratio)
</div>

Prevention:


Error 6: Missing Alt Text

Error Message: “Image has no alt text” (axe-core, Lighthouse)

Common Causes:

Solutions:

Solution 1: Decorative Images

Before: Decorative image without alt

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

After: With empty alt

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

Solution 2: Informative Images

Before: Informative image without alt

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

After: With descriptive alt

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

Before: Image link without accessible name

<a href="/products/123">
    <img src="/product.jpg">
</a>

After: With aria-label on link

<a href="/products/123" aria-label="View product details: Product Name">
    <img src="/product.jpg" alt="" aria-hidden="true">
</a>

Prevention:


Error 7: Incorrect Heading Hierarchy

Error Message: “Heading levels should increase by one” (axe-core, Lighthouse)

Common Causes:

Solutions:

Solution 1: Fix Heading Hierarchy

Before: Skipped heading levels

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

After: Correct hierarchy

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

Solution 2: Single h1 Per Page

Before: Multiple h1 elements

<h1>Main Title</h1>
<h1>Another Title</h1> <!-- Should be h2 -->

After: Single h1

<h1>Main Title</h1>
<h2>Another Title</h2>

Prevention:


Error 8: Missing Semantic HTML

Error Message: “Element has no semantic meaning” (axe-core, manual testing)

Common Causes:

Solutions:

Solution 1: Use Semantic Regions

Before: All divs

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

After: Semantic HTML

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

Solution 2: Use Proper List Markup

Before: Div list

<div class="list">
    <div class="item">Item 1</div>
    <div class="item">Item 2</div>
</div>

After: Semantic list

<ul class="list">
    <li>Item 1</li>
    <li>Item 2</li>
</ul>

Prevention:


Error 9: Dynamic Content Not Announced

Error Message: “Dynamic content changes not announced” (manual testing)

Common Causes:

Solutions:

Solution 1: Use ARIA Live Regions

Before: Status not announced

<div class="status">
    {statusMessage}
</div>

After: With ARIA live

<div
    role="status"
    aria-live="polite"
    aria-atomic="true"
    class="status">
    {statusMessage}
</div>

Solution 2: Use Role Alert for Errors

Before: Error not announced

<div class="error">
    {errorMessage}
</div>

After: With role alert

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

Prevention:


Error 10: Missing ARIA Roles

Error Message: “Element has no programmatic role” (axe-core, Lighthouse)

Common Causes:

Solutions:

Solution 1: Add Role to Custom Components

Before: Custom button without role

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

After: With role

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

Solution 2: Add Role to Custom Controls

Before: Custom toggle without role

<div class="toggle" onclick={handleToggle}>
    <span class="slider"></span>
</div>

After: With role and state

<div
    class="toggle"
    role="switch"
    aria-checked={isChecked}
    aria-label="Enable notifications"
    onclick={handleToggle}
    onkeydown={handleKeyDown}
    tabindex="0">
    <span class="slider"></span>
</div>

Prevention:


WCAG Violation Patterns

Pattern 1: SC 1.3.1 (iii) - Form Label Violations

Violation: Form control has no programmatically associated label

Common Scenarios:

Fix:


Pattern 2: SC 2.1.1 - Keyboard Violations

Violation: Functionality not operable through keyboard

Common Scenarios:

Fix:


Pattern 3: SC 4.1.2 (i) - Name Violations

Violation: Element has no accessible name

Common Scenarios:

Fix:


Pattern 4: SC 1.4.1 - Color Contrast Violations

Violation: Text has insufficient color contrast

Common Scenarios:

Fix:


Prevention Strategies

Development Checklist

Testing Checklist

Code Review Checklist


Q&A

Q: What are the most common LWC accessibility errors?

A: Most common errors include: (1) Missing form labels (inputs without labels), (2) Keyboard inaccessibility (custom components not keyboard accessible), (3) Missing focus indicators (no visible focus styles), (4) Insufficient color contrast (text doesn’t meet 4.5:1 ratio), (5) Missing alt text (images without alt attributes), (6) Incorrect heading hierarchy (skipped heading levels), (7) Missing ARIA roles (custom components without roles).

Q: How do I fix missing form labels in LWC?

A: Fix missing labels by: (1) Adding label attribute to Lightning Base Components (<lightning-input label="Email">), (2) Using <label> element with for/id for custom inputs, (3) Using aria-label when visual label not feasible, (4) Never using placeholder as label replacement. All form controls must have programmatically associated labels.

Q: How do I make custom components keyboard accessible?

A: Make components keyboard accessible by: (1) Adding keyboard event handlers (onkeydown, onkeyup), (2) Using tabindex="0" for focusable elements, (3) Implementing Enter/Space for activation, (4) Implementing Escape for cancellation, (5) Trapping focus in modals (prevent focus from escaping), (6) Returning focus to trigger element when closing.

Q: What are the color contrast requirements for accessibility?

A: Color contrast requirements: (1) Normal text (under 18pt or 14pt bold) - 4.5:1 contrast ratio, (2) Large text (18pt+ or 14pt+ bold) - 3:1 contrast ratio, (3) UI components (buttons, form controls) - 3:1 contrast ratio, (4) Focus indicators - 3:1 contrast ratio. Use WebAIM Contrast Checker to verify contrast.

Q: How do I handle images for accessibility?

A: Handle images by: (1) Adding alt attribute to all images, (2) Using alt="" for decorative images (empty alt), (3) Using descriptive alt text for informative images, (4) Adding aria-label to image links, (5) Using aria-hidden="true" for decorative images. Never omit alt attribute - use empty alt for decorative images.

Q: What is the correct heading hierarchy?

A: Correct heading hierarchy: (1) Start with h1 (one per page), (2) Use h2 for major sections, (3) Use h3 for subsections, (4) Never skip levels (h1 → h3 is wrong, use h1 → h2 → h3), (5) Use headings for structure, not styling. Headings should form a logical outline of the page content.

Q: How do I announce dynamic content changes?

A: Announce dynamic content by: (1) Using aria-live="polite" for status updates (non-urgent), (2) Using aria-live="assertive" for errors (urgent), (3) Using role="status" for status messages, (4) Using role="alert" for error messages, (5) Using aria-atomic="true" to announce entire region. Screen readers will announce changes to live regions.

Q: What ARIA roles should I use for custom components?

A: Use appropriate ARIA roles: (1) role="button" for custom buttons, (2) role="switch" for toggles, (3) role="checkbox" for custom checkboxes, (4) role="dialog" for modals, (5) role="alert" for error messages, (6) role="status" for status messages. Always add ARIA states (aria-checked, aria-expanded, etc.) to match component state.

Q: How do I test LWC accessibility?

A: Test accessibility by: (1) Running automated tests (axe-core, Lighthouse, Jest with @salesforce/sa11y), (2) Testing with keyboard-only navigation (Tab, Enter, Space, Escape), (3) Testing with screen readers (NVDA, JAWS, VoiceOver), (4) Verifying color contrast (WebAIM Contrast Checker), (5) Testing all interactive states (loading, error, success), (6) Testing focus management (focus indicators, focus trapping).

Q: What are best practices for preventing accessibility errors?

A: Best practices include: (1) Always add labels to form controls, (2) Make all interactive elements keyboard accessible, (3) Never remove focus indicators, (4) Use SLDS color tokens for proper contrast, (5) Add alt text to all images, (6) Follow proper heading hierarchy, (7) Use semantic HTML (header, nav, main, footer), (8) Test with screen readers regularly, (9) Include accessibility in code reviews.

Edge Cases and Limitations

Edge Case 1: Dynamic Content with Screen Readers

Scenario: Screen readers not announcing dynamic content changes, causing accessibility issues.

Consideration:

Edge Case 2: Keyboard Navigation in Complex Components

Scenario: Complex components with nested interactive elements causing keyboard navigation issues.

Consideration:

Edge Case 3: Color Contrast with Custom Themes

Scenario: Custom themes or branding causing color contrast violations.

Consideration:

Edge Case 4: Form Validation Accessibility

Scenario: Form validation errors not properly announced to screen readers.

Consideration:

Edge Case 5: Third-Party Component Accessibility

Scenario: Third-party components or libraries not meeting accessibility standards.

Consideration:

Limitations

See Also:

Related Domains: