---
title: "Accessible Lightning Web Components"
url: "https://trailassembly.com/lwc-accessibility/"
topic: "LWC and Front-End"
published: "2026-09-18T08:24:20+00:00"
updated: "2026-10-06T13:57:57+00:00"
---

# Accessible Lightning Web Components

**In one sentence:** use base components and give them real labels, manage focus whenever the DOM changes under the user, and test with the keyboard before you test with anything else. The base component library has done the hard accessibility work already, so most failures in Lightning components come from bypassing it: a clickable `div`, an input labelled only by a placeholder, or a section that appears without telling anyone it did.

## Labels first

Every form control needs a programmatic label, and on base components that is the `label` attribute.

```apex
<lightning-input label="Account name" value={name} onchange={handleChange}>
</lightning-input>

<lightning-input label="Search accounts" variant="label-hidden"
    placeholder="Search" type="search"></lightning-input>
```

`variant="label-hidden"` hides the label visually and keeps it for assistive technology. That is the correct way to build a compact search field. Dropping the label and relying on the placeholder is not: placeholder text disappears the moment the user types, and several screen readers do not announce it at all.

Icon-only buttons need the same treatment through `alternative-text`, and the text should say what the button does rather than what it looks like. “Delete contact”, not “Trash icon”.

```apex
<lightning-button-icon icon-name="utility:delete"
    alternative-text="Delete contact" onclick={handleDelete}>
</lightning-button-icon>
```

## Use the right element

A `div` with an `onclick` is not focusable, is not reachable by keyboard, is not announced as interactive, and does not respond to the space bar. Adding `tabindex="0"` and `role="button"` gets you part of the way and you still have to handle Enter and Space yourself. A `lightning-button` costs less and works.

The same applies to navigation. If it goes somewhere, it should be an anchor with a real `href`, which is what `NavigationMixin.GenerateUrl` exists to produce. Heading levels should describe the document outline rather than be chosen for their font size. Use [SLDS](https://trailassembly.com/lwc-css-and-styling/) text classes to control appearance.

## Focus management

This is where custom components fail most often. When the DOM changes without the user causing the change directly, focus has to be moved deliberately, or a keyboard user is left somewhere that no longer exists.

- A panel or step opens: move focus to its heading or first control.

- A validation error appears: move focus to the first invalid field.

- A row is deleted: move focus to the next row, or to the control that did the deleting.

- Anything closes: return focus to whatever opened it.

```apex
renderedCallback() {
    if (this.shouldFocusFirstField) {
        this.shouldFocusFirstField = false;
        this.template.querySelector('lightning-input')?.focus();
    }
}
```

The guard is required, because `renderedCallback` runs on every render and calling `focus()` on each one steals focus from wherever the user moved it. Base components expose a public `focus()` method. Expose one on your own components too, marked `@api`, so a parent can direct focus without reaching into your shadow tree.

`LightningModal` handles trapping and restoring focus for dialogs, which is a good reason to use it rather than a hand-built overlay.

## ARIA and the shadow boundary

Announce changes that happen without a page transition. A live region does it:

```apex
<div aria-live="polite" class="slds-assistive-text">{statusMessage}</div>
<div role="alert" class="slds-text-color_error">{errorMessage}</div>
```

Use `polite` for status updates such as a result count, and `role="alert"` for errors, which interrupt. The element must be in the DOM before the text changes. Inserting the whole element at once often produces no announcement.

The limitation to plan around: ARIA relationship attributes reference other elements by id, and ids do not cross shadow boundaries. `aria-labelledby` pointing at an id in a different component’s tree resolves to nothing. Keep the label and the thing it labels inside the same component, and where a base component needs a description, use the attribute it exposes rather than trying to wire the ids yourself.

## Testing it

Unplug the mouse. Tab through the component from the top of the page. Every interactive element should be reachable in an order that matches the visual layout, should show a visible focus ring, and should be operable with Enter or Space. Most real defects surface in this pass alone.

Then check colour contrast, which SLDS gets right and custom hex values frequently do not. Then run an automated checker: `@sa11y/jest` plugs into Jest tests for components, and axe or Lighthouse works against the rendered page. Automated tools catch missing labels and contrast failures reliably. They cannot tell you whether focus went somewhere sensible.

## Frequently asked

**Does SLDS make a component accessible by itself?** No. It gives you correct contrast and structure. Labels, focus and keyboard behaviour are still yours.

**Is slds-assistive-text the same as display:none?** No. It positions text off screen so screen readers still read it. `display: none` removes it from the accessibility tree.

**How do I make a custom widget keyboard-navigable?** Follow the ARIA authoring practices pattern for that widget, and use roving `tabindex` so the group is one tab stop with arrow keys inside it.

---

Source: https://trailassembly.com/lwc-accessibility/
Trail Assembly — independent Salesforce write-ups.
