The Guide to Accessible Web Components
Web Components are a newly supported standard. They're a great match for Design Systems because they're futureproof and work with any framework. Building proper UI Web Components can be quite a task though, especially if you want them to be accessible. Here are some pointers on what to look out for.
- What are Web Components?
- What is shadow DOM, and light DOM?
- Extending elements
- Accessible UI Components
- The Accessibility Object Model
- Further reading
Web Components are a set of standards:
- Custom Elements: A way to create your own HTML elements
- HTML Templates: Reusable fragments of DOM
- Shadow DOM: Encapsulation of DOM
Together these standards enable "Reusable extendable encapsulated self-contained components for the web". That's quite a mouthful, but not very clear.
In practice, this means you can create your own HTML elements. These elements have their own bit of encapsulated DOM. Which means they can't be touched or influenced by whatever is outside of the element. They can't be accidentally styled, and they won't mess with your global styles either. And because it's an ES Module, the whole element can be distributed and extended. All these aspects together make up a Web Component.
<script src="good-map.js" async defer></script>
After that, you can use your custom element anywhere on the page.
<good-map api-key="AIzaSyAQuo91bcoB-KwWXaANroTrzpNZRFcNJ1k" latitude="52.1664" longitude="5.9075" zoom="3"></good-map>
Notice how the element name has a dash? That's part of the Custom Elements specification and makes it easier for the browser to recognize them.
A not so practical example would be a spacer-gif Web Component.
<spacer-gif height="1" width="1"></spacer-gif>
A Lion example could be
<lion-switch label="Label Text" help-text="Help text"></lion-switch>
And all that goodness is based on widely supported web standards.
"Indeed, that sounds pretty good so far, but what's the catch?"
As the British will soon discover, there are some downsides to isolating yourself. Let's say you make a card component. The interface for using it could look like this:
<my-card>This is the card content</my-card>
The component, when inspected in the browser, could look like this:
<my-card> #shadow-root (open) <div class="card-wrapper"> <div class="card-header"> Presenting the card content: </div> <div class="card-content"> <slot> <#text>↴ </slot> </div> </div> This is the card content </my-card>
A whole chunk of DOM is rendered by the component and put into the shadow-root section. Inside the
<slot> it refers to the content we put into the element in the first place. All the DOM that is added, is shadow DOM. All the other "normal" DOM, is what we call light DOM. It's the part that's always visible.
As the shadow DOM is completely encapsulated and isolated, it is also completely disconnected. It's almost like it's a completely different document like an iframe.
This becomes a challenge when you want to point a label to an input to create an explicit relationship. In plain HTML, this would be:
<label for="example-input">Label text</label> <input id="example-input" type="text">
When one of both (the
label or the
input) is in the shadow DOM, they're in a completely different context. This makes it impossible to refer to eachother.
This same dillema also goes for WAI-ARIA attributes like
aria-describedby and other that reference an ID. You need either both elements in the shadow DOM, or both of them in the light DOM. Light DOM does not mean that they both have to be in the same slot though.
lion-input we let the developer declare a label in the label slot. This label ends up in the light DOM.
<lion-input> <label slot="label">Label text</label> </lion-input>
The component places an input in a
slot="input", help text in
slot="help-text" and feedback in
slot="feedback". This means the input can be connected to the label, but also that we can use
aria-describedby to connect the input to help text like instructions and feedback like error messages.
As it stands right now, it is only possible to create a Web Component by extending a generic HTML element (
HTMLElement) or another Web Component (which should be somewhere deep down, also an extension of
For accessibility, it could have been a big win if we could extend any element. Imagine you could, for example, extend a native button (
HTMLButtonElement). You would inherit all its behaviour and it's semantics, and you would only add on to that. You'd have a solid fundament upon which you could build.
The specification exists but Safari has stated to not support this feature. Part of the beauty of Web Components is that it's a supported standard. So even though there is a Polyfill for Safari, it creates a path with future uncertainty.
The most popular usecase for Web Components is probably that of creating custom user interface controls. As we can't extend any native elements, we often end up with either wrapping a native element, or recreating its behaviour by ourselves. Wrapping is often the easiest and most solid solution. Recreating is basically the same as taking a
<div> as a starting point.
There are so many aspects that come together in a single components, that it is really easy to overlook a feature or behaviour. And when you forget or fail to implement something, you end up creating something that's lacking compared to a native element. That's probably the exact opposite of what you're trying to achieve.
Here is an overview of aspects that need special attention when creating an accessible user interface control. These points are not specific to Web Components. They are just as relevent for React, Vue, Svelte or any other framework.
If your custom control is interactive, make sure it is keyboard focusable. For simple controls with a single interactive element, this means adding
tabindex='0' to your control. For complexer controls you might need to implement a roving tabindex or use
Users should be able to use your interactive control with a keyboard. For many design patterns, suggested keyboard interactions can be found in the WAI ARIA Authoring Practices.
Interactive controls have several states like focus, hover and active. These should all be clearly visible, and, preferably, each have their own distinctive styling.
An interactive control can have functional states as well. For example, a disclosure widget (or expandable, accordion, expando, ...) can be open or closed. This state needs to be not just visual, but communicated in code as wel. This can be done by toggling
aria-expanded on your control.
The same goes for properties like
aria-multiline. They communicate properties that might be implicit in native elements, that have to be added manually for assistive technology when you're building custom controls. WAI-ARIA has many states and properties to aid in this.
Native HTML elements have a semantic meaning and are mapped to WAI-ARIA roles. A custom element starts out with no role at all, but you can assign one explicitly. WAI-ARIA offers a wide range of roles that should cover all use cases.
Interactive controls must have a name for them to be identified by. For example, a
<button> with the text "Save" can be presented by assistive technology as "Save, button". In this case "Save" is the accessible name of the element. The name is determined by the Accessible Name and Description Calculation and there are multiple ways of adding an accessible name.
Visually, it might be clear that certain elements have a relationship. For example, a short text next to an input will likely be the label of that input. Not clarifying those relationships in code can make it impossible for assistive technology to recognize them though. WCAG Success Criterion 1.3.1 mentions quite some sufficient techniques to cover this issue.
Creating custom elements requires awareness of global standards and conventions. Users expect components to work in a certain way. Reinventing the wheel often leads to a confusing user experience. Following standards and conventions will prevent confusion and create a consistent experience for users.
To create an element that works the same way on each browser and platform is a big challenge. Some native elements even fail to do so. For example, when I use a
<select> in Firefox on Mac OS, it will behave differently from when I open it in Chrome. There will even be a difference between Chrome on Mac OS and Chrome on Windows. The nuances and details of making elements work consistently across platforms is a really big challenge.
Bugs can be even harder to find or circumvent. For example, the WAI ARIA Authoring Practices 1.1 recommends using
aria-activedescendant to control focus when using a
role="combobox". That sounds great, untill you discover that this combination doesn't actually work in all browsers.
The Accessibility Object Model (AOM) is a proposed addition to the web platform to make the accessibility API of browsers more transparant and usable for developers. Support for the AOM in browsers would be of great value for Web Components. But as it is still under development and largely unsupported, I'll leave further explanation to others like Hidde de Vries.
It is very much possible to create accessible Web Components. They are ideal for large organizations where a specialized team can make the best building blocks, and give both their developers and users a great consistent experience. It takes a lot of time, knowledge and effort to build these components though. If you'd ask me...
Everybody should use Web Components
Few people should build them
To ease some of that pain, the Web Components I work on professionaly have an open source base layer called Lion. This is a collection of white-label Web Components that you can easily extend, style and customize. They have been built with all the considerations mentioned above. You can view a live demo of all the components, or check them out on GitHub.