To make our web applications accessible we have WAI-ARIA at our disposal. The WAI-ARIA spec includes a bunch of attributes that can be added to DOM elements that provide additional context to tools like screen readers which help blind people read the content of a web site.
<div role="button"></div>
By adding the role attribute to this div, we are letting screen readers interpret this div as another button. This is a step in the right direction, however we don't pick up all the traits of the button element that make it more accessible than a div.
button
HTMLButtonElement
allows the user to navigate via keyboard by default. When the user presses the tab key on the keyboard, the button will get focus.
If you listen for a click event on the button, this event will also fire when the user presses the Enter
key. This functionality is baked into the button to make it more accessible for users who cannot navigate a site with a mouse, but instead rely on a keyboard.
button.addEventListener('click', onButtonClick)
The only downside to using a button over a div is that it takes some additional styling to override the default look and feel of the button element. This is a small impediment to development compared to the blocker we are presenting for the end user who can't use a div with the keyboard.
Customized built-in elements
What if we want to add even more functionality to the button but retain all the accessibility of HTMLButtonElement?
Customized built-in elements to the rescue!
In this example, we use the fetch API to make a request and style the button based on if that request is successful or has an error. This demonstrates how to use the connectedCallback
lifecycle hook with custom elements v1 API to add an event listener for click, then make the request and based on the result of the request call either one of the custom methods (onSuccess
and onError
) defined on the class.
class MyButton extends HTMLButtonElement {
constructor() {
super();
}
connectedCallback() {
this.addEventListener('click', this.onClick);
}
onClick() {
fetch('http://example.com/some.json')
.then(response => this.onSuccess)
.catch(error => this.onError);
}
onSuccess() {
this.classList.add('is--success');
}
onError() {
this.classList.add('is--error');
}
}
customElements.define('my-button', MyButton, { extends: 'button' })
The last line of this example allows the browser to interpret elements as an extension of the HTMLButtonElement. The main difference here from an autonomous custom element is the third argument, where we pass in an object with an extends
property.
To use the new customized built-in element in a template, we use it like any other button but with a new is
attribute. This attribute tells the browser to create an instance of the MyButton
class after the document has been parsed.
<button is="my-button"></button>
Voilà! Now we have a custom button element that is accessible via the keyboard. Essentially what is going on here is the browser to treating our class like a mixin, combining it's functionality with that of the default button element.
Compatibility with JavaScript frameworks
Support for customized built-in elements is shaky in various JavaScript frameworks. Angular for instance doesn't handle this special use case for the 'is' attribute and doesn't compile the customized built-in element as you might expect. This is a tricky use case, because the browser interprets the 'is' attribute as the document is rendered, not after a JavaScript framework bootstraps or adds DOM to the document. IMHO JavaScript frameworks should also interpret the 'is' attribute, as customized built-in elements promote accessibility which is at times lost in the development process.
Dynamic customized built-in elements
To overcome this limitation if it exists in your framework of choice, you could dynamically create a customized built-in element and add it to your template using document.createElement
. This method takes a second argument that lets the browser interpret this new element as an instance of our MyButton
class.
const myButtonInstance = document.createElement('button', { is: 'my-button' });
this.template.appendChild(myButtonInstance);
This approach has some limitations if the framework needs to bind to the custom element's attributes or content, but nonetheless this method works to render customized built-in elements dynamically.
Browser compatibility
As of March 2019, evergreen browsers partially support the custom elements v1 spec, preferring autonomous custom elements over customized built-in elements. Only Chrome and Firefox support customized built-in elements out of the box. Microsoft has scoped support in Edge, however WebKit is vowing never to support this spec. This is a shame really. This engineer can't really grasp why Apple would hold back an API that is so helpful for implementing accessibility on the web. For browsers that do not support customized built-in elements, this polyfill is required.
Conclusion
By making our web applications more accessible we open up the internet to people who can't navigate with a mouse or touch device. Imagine if you could only use a keyboard to navigate a web app or could only navigate around with voice commands. It would be a frustrating mess if you couldn't effectively use the web app. Customized built-in elements allow you to mixin functionality with elements that already provide features for accessibility. Use customized built-in elements in the course of developing web apps to make the internet a more accessible place.