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. Decorate the private property with @track to make the property reactive. If the property’s value changes, the component re-renders.

This example exposes title as a public property. It converts the title to uppercase and uses the tracked 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, track } from 'lwc';

export default class MyComponent extends LightningElement {
    @track 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.

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 reactive 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, track } from 'lwc';
export default class UiButton extends LightningElement {
    @track innerLabel;

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

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

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

Note

ARIA attributes use camel-case in accessor functions. For example, aria-label becomes ariaLabel.

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

The linking between your IDs and ARIA attributes happen automatically if they’re in the same template. If your attributes are in different templates, you must link them manually.

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