SfSkills Domains Docs GitHub

Home / lwc / lwc-accessibility

lwc-accessibility

Use when designing or reviewing LWCs for keyboard access, semantic labeling, focus. Triggers: LWC accessibility, keyboard nav, aria-label, aria-labelledby, aria-describedby, label for id, focus order, delegatesFocus, tabindex, shadow DOM label association, live region, screen reader. NOT for specific ARIA/datatable implementation patterns — use lwc/lwc-accessibility-patterns. NOT for SLDS styling with no assistive-tech impact — use lwc/lwc-css-and-styling.

Open the skill folder on GitHub


Use this skill when a Lightning Web Component looks correct visually but may fail for keyboard users or assistive technology. The highest-value move in LWC accessibility work is usually to remove custom interaction code and return to accessible base components, then handle the few remaining focus and labeling gaps deliberately.


Before Starting

Gather this context before working on anything in this domain:


Questions to Ask Before Configuring

Ask these before writing markup. Each one maps to a failure in references/gotchas.md that renders without an error and is invisible in a mouse-only demo.

AskWhy it mattersWhat a good answer adds
"Can lightning-input / lightning-combobox / lightning-record-edit-form express this control, and if not, exactly what stops it?"Base components already associate the label, update ARIA on interaction, and meet WCAG 2.1 AA contrast; a custom control re-inherits all of thatA written justification for custom markup, or the decision not to build it
"Which component owns the label, the help text, and the error text?"for / aria-describedby link only inside one template; across templates in native shadow DOM they cannot link at allThe component boundary drawn around the whole form element, not around each visual part
"Where must focus be after open, after a validation failure, after save, and after close?"Focus skips the custom element container by default, so an unstated contract means focus goes nowhereFour named focus targets that become four Jest assertions
"Does a parent need to call focus() on this component?"That decides delegatesFocus vs an @api focus method — and delegatesFocus forbids tabindex inside the componentThe focus mechanism chosen once, up front, instead of both being half-applied
"Which state changes must be announced without moving focus?"Save confirmations, async result counts, and inline validation are silent to a screen reader unless a live region carries themThe list of announcements and which are polite (role="status") vs assertive (role="alert")
"Which icons carry meaning, and which are decorative?"alternative-text on a decorative icon is noise; its absence on a meaningful one is a WCAG 1.1.1 failureA per-icon decision instead of a blanket rule applied in either direction
"Will this run in Experience Cloud, Lightning Out, or a native-shadow context?"Lightning Experience and Experience Builder use synthetic shadow today; outside them LWC renders in native shadow, where cross-template id linking is impossibleThe environment list that decides whether a cross-component ARIA reference can ship at all

What a proper accessibility design adds over "adding ARIA at the end": the label association survives id transformation, focus has a named destination at every state change, the keyboard contract is asserted by tests rather than by a manual pass, and the component does not silently degrade when the org moves from synthetic to native shadow.


Core Concepts

Accessibility in LWC is easiest when the component stays close to platform primitives. The farther a team moves toward clickable div elements, custom focus logic, and manual ARIA, the more likely it is to recreate a solved problem badly.

PrincipleDefaultAnti-patternWhy it matters
Base components firstUse lightning-* base components or SLDS blueprintsCustom menu/button/toggle in raw HTMLBase components ship tested keyboard, label, and AT support
Accessible nameReal button text, label on inputs, alternative-text on meaningful iconsARIA-patching a clickable span or divARIA cannot repair structurally wrong markup
Focus contractLand on first actionable element, trap inside modals, return to launcher on closeLetting focus stay wherever the browser last placed it after rerender/saveLost focus = component is broken for keyboard users
Programmatic validationUse base-component validation; programmatic error association on custom inputsError state shown only in color or layout textColor-only errors fail screen readers and color-blind users
One template per form elementLabel, control, help text, and error text in the same templateSplitting the label into its own component for reuseId-based association is linked automatically only inside one template
One focus mechanismdelegatesFocus for a field wrapper, an @api method for a composite widgetdelegatesFocus plus tabindex in the same componentThe guide states the two combined throw off focus order

Common Patterns

Base-Component Replacement For Custom Click Targets

When to use: The component currently uses custom HTML such as a clickable div, icon-only action, or hand-rolled toggle.

How it works: Replace the interactive surface with lightning-button, lightning-button-icon, lightning-input, or another standard base component. Keep any remaining custom markup decorative, not interactive.

Why not the alternative: Adding tabindex, role, and key handlers to arbitrary markup usually creates incomplete keyboard behavior and inconsistent screen-reader output.

Deliberate Focus Management Around Dialogs

When to use: The component opens a modal, quick action surface, or blocking overlay.

How it works: Use LightningModal or another supported dialog surface, set focus on meaningful content or the first actionable control, and return focus to the launch element when the interaction closes.

Why not the alternative: Leaving focus wherever the browser last had it makes the overlay hard to use and easy to lose.

Accessible Composite Widget Boundary

When to use: A real business need requires a custom picker, listbox, or multi-step composite component.

How it works: Choose a known WAI-ARIA interaction pattern, document the keyboard contract, and test it with both tab order and screen-reader announcements before shipping.


Decision Guidance

SituationRecommended ApproachReason
Standard action, toggle, or inputUse a lightning-* base componentAccessible behavior is already implemented and maintained by the platform
Need branded layout but normal semanticsUse an SLDS blueprint with minimal custom behaviorKeeps semantics closer to supported interaction models
Need a modal or blocking overlayUse LightningModal with an explicit focus planModal semantics and dismissal behavior are easier to keep correct
Considering ARIA on a clickable divReplace it with semantic HTML or a base componentARIA does not fully repair incorrect structure
Custom composite widget is unavoidableImplement and test a formal keyboard modelComposite controls need a documented accessibility contract
Label must live in a different component from its inputRedesign to one template, or render both in light DOMCross-template id linking is impossible in native shadow DOM; light DOM is the guide's stated remedy and carries a security cost
Parent needs to focus a child componentSet delegatesFocus, or expose an @api focus methodFocus skips the custom element container, so the host's focus() does nothing by default
Custom cell type inside lightning-datatableWire data-navigation="enable" and internalTabIndex through the cell templateCustom data types do not participate in the datatable's keyboard navigation until they opt in

  1. Inventory the interactive surface. List every element a user can activate, and for each one

    record: is it a base component, semantic HTML, or a div/span with a handler? Fill in the Component Scope and Accessibility Contract sections of templates/lwc-accessibility-template.md as you go — the unanswered lines are the design gaps.

  2. Delete before you add. For each custom interactive surface, answer the first question in

    Questions to Ask. Anything a lightning-* component can express goes back to that component; the remainder is what you are actually designing.

  3. Draw the labelling boundary. Put the label, control, help text, and error text of one field

    in one template. Read references/gotchas.md § Id-Based ARIA Association Stops At The Shadow Boundary before splitting anything across components, and § Id-Referencing ARIA Attributes Are Not Ordinary Reflected Properties before exposing an aria-* property with @api.

  4. Build from the reference bundle. Copy the component in references/code-examples.md — HTML,

    JS, CSS, js-meta.xml — and its package.xml. Start from templates/lwc/component-skeleton/ for the bundle shape. Pick one focus mechanism (delegatesFocus or explicit tabindex), never both.

  5. Assert the accessibility, don't eyeball it. Port the Jest suite from

    references/code-examples.md: for/id relationship, aria-describedby target, aria-invalid flip, live-region text, shadowRoot.activeElement after an error, and zero tabindex when delegatesFocus is set. Run with npm run test:unit.

  6. Run the checker over the source tree.

    python3 skills/lwc/lwc-accessibility/scripts/check_lwc_accessibility.py --manifest-dir force-app/main/default/lwc It fails on unresolvable id references (A4), unsupported tabindex values (A5), tabindex beside delegatesFocus (A6), unlabelled controls (A2, A7, A9), and incomplete bundles (B1, B2). Every finding is a real defect, not a style opinion.

  7. Do the two things no script can do. Tab the whole component with the mouse untouched, and

    run it once with a screen reader, checking the four focus destinations from step 3 of Questions to Ask. Record the result in the Sign-Off Checklist of the template.


Review Checklist

Run through these before marking work in this area complete:


Salesforce-Specific Gotchas

Non-obvious platform behaviors that cause real production problems:

  1. lightning-icon needs deliberate text when it carries meaning - icon-only affordances look obvious visually but become vague or silent to assistive technology without alternative-text.
  2. Custom HTML can regress below the platform baseline quickly - moving away from lightning-* components often removes built-in keyboard and label behavior teams assumed they still had.
  3. Focus can get lost after rerender - conditional templates, async state changes, and modal open or close transitions can strand keyboard users unless focus is restored deliberately.
  4. ARIA does not replace semantic structure - adding roles to the wrong element can still produce confusing navigation and announcement behavior.
  5. Template id values are rewritten at render time - #myInput selectors in JS and CSS miss; only for / aria-* associations survive, and only within one template.
  6. for and aria-labelledby do not cross a shadow boundary - the attribute renders, nothing links, and no error appears.
  7. tabindex supports only 0 and -1 - and must not be combined with delegatesFocus.
  8. A custom element is not focusable by default - focus() on the host is a no-op until you set delegatesFocus or expose a focus method.
  9. Base components always render in shadow DOM - switching your component to light DOM does not open up theirs.
  10. lightning-datatable custom cell types opt out of keyboard navigation - until data-navigation and internalTabIndex are wired through.

Full detail, with guide line citations, is in references/gotchas.md.


Output Artifacts

ArtifactDescription
Accessibility reviewFindings on semantic markup, labels, keyboard interaction, and focus behavior
Remediation planConcrete changes to base components, ARIA usage, and focus handling
Test checklistKeyboard and screen-reader scenarios that should pass before release
Component bundleHTML / JS / CSS / js-meta.xml with in-template label and error association (references/code-examples.md)
Jest suiteAssertions on for/id, aria-describedby, aria-invalid, live-region text, and focus destination
Checker reportcheck_lwc_accessibility.py --manifest-dir <lwc root> output, clean before deploy

Reference Files

FileRead it when
references/code-examples.mdYou are building or fixing a bundle — accessible input wrapper (HTML/JS/CSS), js-meta.xml, Jest suite, package.xml, deploy and verification commands
references/gotchas.mdSomething renders correctly and behaves wrongly: id rewriting, cross-boundary ARIA, tabindex, delegatesFocus, datatable cell types, synthetic vs native shadow
references/examples.mdYou want the worked before/after: clickable card → button, focus return after a modal, and the label-across-two-components failure with both fixes
references/llm-anti-patterns.mdYou are reviewing AI-generated LWC markup, or you are the AI generating it — eight detection hints for the mistakes that pass a visual check
references/well-architected.mdYou are justifying the design in a review: pillar framing, tradeoffs, and the official sources with the claim each one supports
templates/lwc-accessibility-template.mdYou are running the review — the scope, contract, labelling, and sign-off worksheet
scripts/check_lwc_accessibility.pyBefore every deploy — --manifest-dir <lwc source root>

Package files