# 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>
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 {
}
# 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.
- Adds focus to the native button HTML element using
coolButton.focus()
. - If you click a node inside the shadow tree and the node isn’t a focusable area, the first focusable area becomes focused. This behavior is similar to when you click a label and focus jumps into an input.
- When a node inside the shadow tree gains focus, the
:focus
CSS selector applies to the host in addition to the focused element.
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"; }
}
# Link IDs and ARIA Attributes from Different Templates
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.
- Receives information from the child
carouselImage
component about its Id so that we can set it on thearia-controls
attribute. - Sends down the pagination Id to
carouselImage
so that it can set thearia-labelledby
attribute.
// 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:
ariaActiveDescendant
ariaControls
ariaDescribedBy
ariaDetails
ariaErrorMessage
ariaFlowTo
ariaLabelledBy
ariaOwns
These non-standard properties are no longer included as a global polyfill in LWC v4. For accessing ARIA attributes on HTML Element
s, 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.