Not verified hands-on. This article was written from primary documentation rather than reproduced in an org, so it carries no tested-release badge. If you run it and something differs, say so in the comments and it gets tested and corrected.

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.

<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”.

<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 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.
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:

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