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.
Top comments (4)
I think it would be nice to mention that this will "forever" require a "polyfill".
Customized Built-In Elements are part of the spec indeed but Webkit e.g. all Apple products will not implement it.
Therefore such elements will "never" run natively on Iphone, Mac or Safari.
Some more details github.com/open-wc/open-wc/issues/...
Nothing has to be forever. Maybe a nicely worded open letter to Tim Cook about how Apple engineers are holding back accessibility on the web by mishandling this particular spec would help. That being said, post is edited to be more specific about WebKit.
thx :) so if you write that letter - you can also add my sign :)
I wanted to share an easy-to-use alternative that works well in place of customized builtins:
It is a lib, so I guess it is "polyfilled" forever at this point. It is easy to install though, and without the complications of "upgrading" element classes.
The class definition format is very similar, and it uses a
has
attribute instead ofis
attribute, andhas
takes a space-separated list so any number of functionalities (behaviors, defined with classes using the same life cycle methods as custom elements) can be instantiated on an element.