Linting Salesforce Lightning Web Components (LWC)
Build a component configuration step by step, using Salesforce Lightning Web Components as a worked example
Salesforce Lightning Web Component (LWC) templates are HTML files built from custom elements: Salesforce base components such as <lightning-button> and <lightning-icon>, and the components you author yourself in the c- namespace, such as <c-my-image>. Axe DevTools Linter checks custom elements once you tell it what those elements render, using the global-components configuration option.
This article builds an LWC configuration from scratch. LWC makes a good worked example because it exercises every part of the mapping syntax, including components that render no element of their own. The method applies to any component library: describe what each component renders, verify that description against a test file, then correct the mapping wherever the two disagree.
For an introduction to custom component mapping, see Linting Custom Components. For the complete syntax reference, see Configuring Axe DevTools Linter.
Before You Start
Add an axe-linter.yml file to the root of your project. Every example below goes in that file. If you use the REST endpoint instead of an editor extension, the same configuration goes in the request's config object, as shown in Linting Custom Components with the Axe DevTools Linter REST Endpoint.
A Mapping Describes What the Browser Receives
This is the idea that makes every other decision straightforward: a mapping describes the element your component renders, not the tag you write in the template.
<lightning-button label="Save"> renders a button whose text content is "Save". Written as a mapping, that is:
global-components:
lightning-button:
element: button
attributes:
- label: <text>The special <text> value says that the label attribute becomes the element's text content, which is what gives a button its accessible name. With that mapping in place, Axe DevTools Linter reports a button-name violation for <lightning-button> used with no label, exactly as it would for an empty <button>.
Step 1: Map the Components That Render an Element
Start with components that map cleanly onto a single native element, and map only the attributes that carry accessibility information.
global-components:
# Salesforce base components
lightning-button:
element: button
attributes:
- label: <text>
lightning-icon:
element: img
attributes:
- alternative-text: alt
# Components you author yourself
c-my-image:
element: img
attributes:
- alternative-text: altComponent names are case-sensitive and match the tag as you write it in the template, so LWC's kebab-case names are used as-is.
For the other special values (aria-* to pass ARIA attributes through, <element> to let an attribute choose the emitted element, and default to declare a value the component always renders) see the walkthrough in Linting Custom Components with the Axe Accessibility Linter Extension for VS Code or the JetBrains Plugin.
Step 2: Declare the Wrappers That Render Nothing
LWC uses <template> for a component's root element and again for iteration and conditionals:
<template>
<ul>
<template for:each={items} for:item="item">
<li key={item.id}>{item.name}</li>
</template>
</ul>
</template>In HTML, the contents of a <template> element are inert: the browser does not render them, and assistive technology cannot reach them. Axe DevTools Linter evaluates the markup a user actually gets, so it does not report on content inside a <template>. LWC uses the same tag name for a compile-time instruction that produces no element at all, so the linter needs to be told what these wrappers become. The markup above reaches the browser like this:
<ul>
<li>…</li>
<li>…</li>
</ul>Declaring template in your configuration is what lets the linter see through those wrappers to the markup inside. Because a mapping has to name some element, and a mapping applies everywhere the tag appears, the element you pick matters. Working through two choices with the list above shows why.
Trying a Generic Container
A div is the natural first guess, since a wrapper sounds like a generic container:
global-components:
template: divAxe DevTools Linter now evaluates the list as though you had written the <template> as a div:
<ul>
<div>
<li>…</li>
</div>
</ul>And it reports a violation on the <ul> line:
list: <ul> and <ol> must only directly contain <li>, <script> or <template> elementsThe report is correct about the markup it was given, but that markup is not what the browser receives. LWC removes the wrapper, so the rendered list contains list items directly and is perfectly valid. The violation comes from the substitute element, not from your template.
Matching the Rendered Relationship
Now map template to the element the browser actually finds inside the list:
global-components:
template: liThe linter evaluates the same list like this:
<ul>
<li>
<li>…</li>
</li>
</ul>No list violation is reported, because the <ul> directly contains an li, which is one of the elements the rule message above allows, and it is the relationship the browser gets as well. The doubled li exists only in the linter's view of your file: it stands in for a wrapper that renders nothing, and you never write it in a template.
Leaving template out of your configuration also avoids the violation, but then nothing inside your templates is evaluated at all, which is the problem Step 2 set out to solve.
Both Choices Check the Content Inside
The substitute element affects only how the wrapper itself appears to the linter. The markup inside is evaluated either way. Add an image with no alternative text to the same list:
<template>
<ul>
<template for:each={items} for:item="item">
<li key={item.id}><img src={item.url}></li>
</template>
</ul>
</template>Both template: div and template: li report the missing alternative text on the image:
image-alt: Images must have alternative textWith template: div you get that result plus the spurious list violation, which is the practical difference between the two. Because iterating list items is the most common use of <template>, mapping it to li matches the rendered output most often, and it is the recommended starting point.
Three rules check that a container holds the right kind of direct child: list for <ul> and <ol>, definition-list for <dl>, and summary-name for <details>. Where a <template> sits directly inside one of those four containers, your substitute element takes its place, so these three rules may not report what they would for the equivalent plain HTML. No other rule is affected, and the content inside the template is still checked in full.
Step 3: Verify Your Configuration
Do not assume a mapping behaves the way you intended. The most reliable check is to write the same markup twice, once with your components and once as the plain HTML you expect them to render, and confirm that both produce the same results.
Create a small test file, scratch.html, anywhere in the project that Axe DevTools Linter checks:
<template>
<c-my-image src="cat.jpg"></c-my-image>
<lightning-button></lightning-button>
</template>Then create its plain-HTML twin, scratch-expected.html, containing the markup you expect those components to render:
<img src="cat.jpg">
<button></button>With the mappings from Steps 1 and 2 in place, both files report the same two violations: image-alt for the image with no alternative text, and button-name for the button with no accessible name. Matching results mean the mappings describe your components correctly. A violation in one file but not the other points at the configuration rather than at your markup.
This pair also shows why the check is worth doing. Without the template mapping from Step 2, scratch.html reports nothing at all while scratch-expected.html reports both violations, and that mismatch is the signal that something in the configuration is missing.
There are three convenient ways to see the results:
- In VS Code or a JetBrains IDE, open the file and read the highlighted errors, as described in Linting Custom Components with the Axe Accessibility Linter Extension for VS Code or the JetBrains Plugin.
- With the Axe DevTools Linter Connector, run it against both files in one command.
- With the REST endpoint, post each file's contents with your configuration in the
configobject. See Linting Custom Components with the Axe DevTools Linter REST Endpoint.
Adding the component name to each result makes larger configurations much easier to debug, because it shows which mapping produced a violation. See Analyzing Custom Component Violations.
Deciding What a Mapping Should Require
Some components render different markup depending on how they are used, and mapping them is a policy decision rather than a simple translation. <lightning-icon> is a good example: an icon with no alternative-text is decorative, while an icon with alternative-text conveys meaning.
Mapping <lightning-icon> to img means the linter asks every icon to declare its alternative text. An explicitly empty value satisfies that request:
<!-- Reported: an image with no alternative text -->
<lightning-icon icon-name="utility:check"></lightning-icon>
<!-- Not reported: explicitly decorative -->
<lightning-icon icon-name="utility:check" alternative-text=""></lightning-icon>That is a useful convention, because it makes each icon's intent visible in the template instead of implied by omission. If you would rather not adopt it, leave <lightning-icon> out of your configuration and the linter will not evaluate it.
When a component always renders the same value for an attribute, such as a fixed role, use default to record that value. A default takes effect only when the value is not empty, and a default can set an attribute but cannot supply <text> content. See Default Attributes.
Map only the attributes that carry accessibility information, and confirm what each component renders before mapping it. The examples in this article follow the markup the Salesforce base components produce, but your own components, and any component you wrap, need the same check.
Applying This to Your Own Components
The steps above generalize to any component library:
- List the components that render an interactive or meaningful element, such as buttons, links, images, form controls, and headings. Those give you the most value for the least configuration.
- For each one, note what it renders and which attributes carry accessibility information, then map only those attributes.
- Declare wrappers that render nothing of their own, choosing a substitute element that preserves the relationship the browser sees.
- Verify each mapping against its plain-HTML equivalent before relying on it.
- Revisit the configuration when your components change. A mapping only reflects how a component behaved on the day the mapping was written.
If your library is one of the libraries Axe DevTools Linter already knows about, you can skip most of this work. See Preconfigured Component Libraries.
