Accessibility

Many users rely on a keyboard to navigate the web. Pressing the Tab key moves focus through elements on the page. These users need a visual cue to know which element on the page has focus. As a developer, you need to ensure that when a user tabs through a page, the browser moves focus to the next logical element.

People with low vision also use screen readers to navigate web pages. Screen readers read aloud the elements on the page, including the element that has focus. In some cases, you must use attributes to describe the elements on the page to the screen reader.

Focus

Handle Focus Manually

When a user tabs through a page, interactive elements like <a>, <button>, <input>, and <textarea> receive focus automatically. To allow elements that are not natively focusable, such as <div> or <span>, to receive focus, assign them tabindex="0".

Note

Only 0 and -1 tabindex values are supported. Assigning tabindex="0" means that the element focuses in standard sequential keyboard navigation. Assigning tabindex="-1" removes the element from sequential keyboard navigation. For more information, see Keyboard Accessibility.

It’s important to understand that focus skips the component container and moves to the elements inside the component. In this example, when a user tabs, focus moves from a button element in parent to an input element in child, skipping child itself.

<!-- parent.html -->
<template>
    <button>Button</button>
    <example-child></example-child>
</template>
<!-- child.html -->
<template>
    <span>Tabbing to the custom element moves focus to the input, skipping the component itself.</span>
    <br /><input type="text">
</template>

Playground output with custom component selected.

To add focus to a component, use the tabindex attribute. In the parent template, set the child component’s tabindex to 0 to add the child component itself to the navigation sequence.

<!-- parent.html -->
<template>
    <button>Button</button>
    <example-child tabindex="0"></example-child>
</template>
<!-- child.html -->
<template>
    <span>Tabbing to the custom element moves focus to the whole component.</span>
    <br /><input type="text" />
</template>
// child.js
import { LightningElement } from 'lwc';

export default class Child extends LightningElement {

}

Playground output with whole custom component selected.

Handle Focus Automatically

Instead of setting focus manually, you can manage focus automatically. In a component’s JavaScript class, set delegatesFocus to true.

Using delegatesFocus enables the following cases.

Note

Don’t use tabindex with delegatesFocus because it throws off the focus order.

<!-- coolButton.html -->
<template>
    <button>Focus!</button>
</template>
// coolButton.js
import { LightningElement} from 'lwc';

export default class CoolButton extends LightningElement {
    static delegatesFocus = true;
}

Accessibility Attributes

To make your components available to screen readers and other assistive technologies, use HTML attributes on your components that describe the UI elements that they contain. Accessibility software interprets UI elements by reading the attributes aloud.

One critical piece of accessibility is the use of the title attribute. Screen readers read title attribute values to a user. When you consume a Lightning web component with a title attribute, always specify a value. For example, the ui-button component has a title attribute.

<!-- parent.html -->
<template>
    <ui-button title="Log In" label="Log In" onclick={login}></ui-button>
</template>

That template creates HTML output like the following for the screen reader to read out Log In to the user.

<!-- Generated HTML -->
<ui-button>
   <button title="Log In">Log In</button>
</ui-button>

When you’re creating a Lightning web component, use @api to expose a public title attribute if you want a screen reader to read a value aloud to the user.

When you take control of an attribute by exposing it as a public property, the attribute no longer appears in the HTML output by default. To pass the value through to the rendered HTML as an attribute (to reflect the property), define a getter and setter for the property and call the setAttribute() method. (To hide HTML attributes from the rendered HTML, call removeAttribute().)

You can also perform operations in the setter. Use a private property to hold the computed value.

This example exposes title as a public property. It converts the title to uppercase and uses the private property privateTitle to hold the computed value of the title. The setter calls setAttribute() to reflect the property’s value to the HTML attribute.

// myComponent.js
import { LightningElement, api } from 'lwc';

export default class MyComponent extends LightningElement {
    privateTitle = '';
    @api
    get title() {
        return this.privateTitle;
    }

    set title(value) {
        this.privateTitle = value.toUpperCase();
        this.setAttribute('title', this.privateTitle);
    }
}
<!-- parent.html -->
<template>
   <example-my-component title="Hover Over the Component to See Me"></example-my-component>
</template>
/* Generated HTML */
<example-my-component title="HOVER OVER THE COMPONENT TO SEE ME">
   <div>Reflecting Attributes Example</div>
</example-my-component>

ARIA Attributes

To provide more advanced accessibility, like have a screen reader read out a button’s current state, use ARIA attributes. These attributes give more detailed information to the screen readers that support the ARIA standard.

You can assign ARIA attributes to id attributes in your HTML template. In a component’s template file, id values must be unique so that screen readers can associate ARIA attributes such as aria-describedby, aria-details, and aria-owns with their corresponding elements.

Note

When a template is rendered, id values may be transformed into globally unique values. Don’t use an id selector in CSS or JavaScript because it won’t match the transformed id. Instead, use the element’s class attribute or a data-* attribute like data-id. LWC id values are used for accessibility only.

Let’s look at some code. The aria-pressed attribute tells screen readers to say when a button is pressed. When using a ui-button component, you write:

<!-- parent.html -->
<template>
    <ui-button title="Log In" label="Log In" onclick={login} aria-label="Log In" aria-pressed></ui-button>
</template>

The component defines the ARIA attributes as public properties, and uses private properties to get and set the public properties.

<!-- ui-button.html -->
<template>
    <button title="Log In" label="Log In" onclick={login} aria-label={innerLabel} aria-pressed={pressed}></button>
</template>

The component Javascript uses the camel-case attribute mappings to get and set the values in ui-button.js. For example, to access aria-label, use ariaLabel.

// ui-button.js
import { LightningElement, api } from 'lwc';
export default class UiButton extends LightningElement {
    innerLabel = '';

    @api
    get ariaLabel() {
        return this.innerLabel;
    }

    set ariaLabel(newValue) {
        this.innerLabel = newValue;
    }

    pressed;

    @api
    get ariaPressed() {
        return this.pressed;
    }

    set ariaPressed(newValue) {
        this.pressed = newValue;
    }
}

So the generated HTML is:

<ui-button>
    <button title="Log In" label="Log In" onclick={login} aria-label="Log In" aria-pressed="true"></button>
</ui-button>

A screen reader that supports ARIA reads the label and indicates that the button is pressed.

Important

ARIA attributes use camel-case in accessor functions. For example, aria-label becomes ariaLabel. Note that to match the specification, ariaLabeledBy maps to aria-labeledby instead of aria-labeled-by. The complete mapping list is defined in the LWC GitHub repo.

Default ARIA Values

A component author may want to define default ARIA attributes on a custom component and still allow component consumers to specify attribute values. In this case, a component author defines default ARIA values on the component’s element.

// ui-button.js sets "Log In" as the default label
import { LightningElement } from 'lwc';
export default class UiButton extends LightningElement {
    connectedCallback() {
        this.template.ariaLabel = 'Log In';
    }
}

Note

Define attributes in connectedCallback(). Don’t define attributes in constructor().

When you use the component and supply an aria-label value, the supplied value appears.

<!-- parent.html -->
<template>
    <ui-button title="Log In" label="Submit" onclick={login} aria-label="Submit" aria-pressed></ui-button>
</template>

The generated HTML is:

<ui-button>
    <button title="Log In" label="Submit" aria-label="Submit" aria-pressed="true"></button>
</ui-button>

And, when you don’t supply an aria-label value, the default value appears.

<!-- parent.html -->
<template>
    <ui-button title="Log In" label="Log In" onclick={login}></ui-button>
</template>

The generated HTML is:

<ui-button>
    <button title="Log In" label="Log In" onclick={login} aria-label="Log In"></button>
</ui-button>

Static Attribute Values

What if you create a custom component and don’t want the value of an attribute to change? A good example is the role attribute. You don’t want a component consumer to change button to tab. A button is a button.

You always want the generated HTML to have the role be button, like in this example.

<ui-button>
    <div title="Log In" label="Log In" onclick={login} role="button"></div>
</ui-button>

To prevent a consumer from changing an attribute’s value, simply return a string. This example always returns "button" for the role value.

// ui-button.js
import { LightningElement, api } from 'lwc';
export default class UiButton extends LightningElement {
    set role(value) {}

    @api
    get role() { return "button"; }
}

For elements in the same template, linking between IDs and ARIA attributes works automatically.

However, linking IDs and ARIA attributes between elements in separate templates is not possible for components using shadow DOM.

To link together two elements using IDs and ARIA attributes, use Light DOM components to place them in the same shadow root. For instance:

<!-- container.html -->
<template>
    <c-label></c-label>
    <c-input></c-input>
</template>
<!-- label.html -->
<template lwc:render-mode="light">
    <label id="my-label">My label</label>
</template>
<!-- input.html -->
<template lwc:render-mode="light">
    <input type="text" aria-labelledby="my-label">
</template>

In the above example, the <input> may reference the <label>'s ID using aria-labelledby because they are both light DOM components contained with the container.html template, which is a shadow DOM component.

Workaround for Synthetic Shadow DOM

Important

This workaround for synthetic shadow DOM is deprecated. This does not work in native shadow DOM. Avoid using this workaround unless absolutely necessary.

In synthetic shadow DOM, it is possible to manually link together elements across shadow roots.

Say we have a component that has an element that controls another component’s behavior, such as a carousel. A consumer component can pass content into the <slot>.

<!-- carousel.html -->
<template>
    <div class="carousel-images">
       <!-- Carousel images go here -->    
       <slot onprivateimageregister={imageRegisterHandler}></slot>
    </div>
    <ul>
        <template for:each={paginationItems} for:item="paginationItem">
            <li key={paginationItem.key}>
                <!-- Dynamic Ids are allowed on for:each iterations -->
                <!-- aria-controls refers to an ID value from a different template -->
                <a id={paginationItem.id}
                   href="javascript:void(0);"
                   aria-selected={paginationItem.ariaSelected}
                   aria-controls={paginationItem.contentId}>
                  {paginationItem.imageTitle}
                </a>
            </li>
       </template>
    </ul>
</template>

To communicate with the child components passed into the slot, the component relies on the custom event privateimageregister. The event handler performs these steps.

// carousel.js
@track paginationItems = [];

imageRegisterHandler(event) {
    const target = event.target,
          item = event.detail,
          currentIndex = this.paginationItems.length,
          paginationItemDetail = {
               key: item.guid,
               id: `pagination-item-${currentIndex}`,
               contentId: item.contentId,
            };

   item.callbacks.setLabelledBy(paginationItemDetail.id);
   this.paginationItems.push(paginationItemDetail);
}

The carouselImage component includes an image with a link.

<!-- carouselImage.html -->
<template>
    <div role="tabpanel" id="carousel-image">
        <a href={href} tabindex={tabIndex}>
            <img src={src} alt={alternativeText}>
            <span>{description}</span>
        </a>
    </div>
</template>

To get the rendered Id value on the div element, retrieve it in renderedCallback(). The aria-labelledby attribute is manually set using setAttribute() because it doesn’t support dynamic values.

renderedCallback() {
    if (this.initialRender) {
        this.panelElement = this.template.querySelector('div');

        const privateimageregister = new CustomEvent(
            'privateimageregister',
                {
                   bubbles: true,
                   detail: {
                       callbacks: {
                           setLabelledBy: this.setLabelledBy.bind(this),
                       },
                       contentId: this.panelElement.getAttribute('id'),
                       guid: guid(),
                   }
                }
            );
        this.dispatchEvent(privateimageregister);
        this.initialRender = false;
    }
}

setLabelledBy(value) {
    this.panelElement.setAttribute('aria-labelledby', value);
}

Deprecated ARIA reflected properties

LWC originally shipped with a polyfill for ARIA string reflection. For historical reasons, this polyfill includes several properties that are now considered non-standard:

These non-standard properties are no longer included as a global polyfill in LWC v4. For accessing ARIA attributes on HTML Elements, we encourage you to use the attribute format instead of the property format, as described below.

Setting an attribute

Example of incorrect code:

element.ariaLabelledBy = 'foo';

Example of correct code:

element.setAttribute('aria-labelledby', 'foo');

Getting an attribute

Example of incorrect code:

console.log(element.ariaLabelledBy);

Example of correct code:

console.log(element.getAttribute('aria-labelledby'));

Removing an attribute

Example of incorrect code:

element.ariaLabelledBy = null;

Example of correct code:

element.removeAttribute('aria-labelledby');

Attribute equivalents

The attribute equivalents for each non-standard property are as follows:

Property Attribute
ariaActiveDescendant aria-activedescendant
ariaControls aria-controls
ariaDescribedBy aria-describedby
ariaDetails aria-details
ariaErrorMessage aria-errormessage
ariaFlowTo aria-flowto
ariaLabelledBy aria-labelledby
ariaOwns aria-owns

Note that, for LightningElement objects, the non-standard properties are still supported, for the sake of both backwards compatibility and ergonomics.