Using Web Components For Progressive Enhancement
When developing enhancement-only features, I find using custom elements makes it easier and more organized. Custom elements are a natural fit for enhancements as they only execute if JavaScript runs.
For example, this site supports a few different themes and a theme picker enhancement is available in the footer of the site.
The default theme for the site is a lighter color scheme, but if the device supports prefers-color-scheme a visitor could be served the darker theme depending on their device preferences.
Because changing the theme is an enhancement, the theme picker elements are marked with the hidden attribute telling browsers that the element is not relevant to the page and should not be presented.
For visitors with JavaScript enabled, the elements allow a theme to be picked and presented. The selected theme is set on the root html element and stored in localStorage so it can be restored.
To encapsulate the code, an autonomous custom element is defined in the customElements registry.
customElements.define('theme-picker', class extends HTMLElement {
connectedCallback() { /* ... */ }
disconnectedCallback() { /* ... */ }
handleEvent() { /* ... */ }
});
The theme-picker element wraps the elements that make up the theme picker. Using a label and select automatically adds support for screen readers and keyboard users.
<theme-picker>
<label hidden>Theme:
<select id="theme-picker" name="theme-picker" hidden>
</select>
</label>
</theme-picker>
When the theme-picker element is rendered, the connectedCallback method is fired and removes the hidden attributes from the label and select causing them to be presented to the visitor. The select is then wired up passing this as the listener. The component's handleEvent method sets and stores the theme.
connectedCallback() {
this.label = this.querySelector('label');
this.picker = this.querySelector('#theme-picker');
if (!this.label || !this.picker) { return; }
this.picker.value = this.getThemeAttribute();
this.label.removeAttribute('hidden');
this.picker.removeAttribute('hidden');
this.picker.addEventListener('change', this);
}
Without JavaScript enabled, the theme picker elements remain hidden and visitors see whatever theme matches their device preference.
All of this could be done without a custom element. Earlier iterations of my theme picking support used standard JavaScript functions that were in the global namespace. By creating a custom theme-picker element I'm able to logically group and contain all theme picking functionality in one spot.
I don't have to worry about collisions with other enhancements or getting overly verbose with naming conventions. When I'm ready to retire an enhancement I can just remove the element. It's all neatly wrapped up together.
The next time you're writing some functionality, take a look at custom elements. Developers who work with React, Angular, or similar frameworks will find custom elements quite familiar. Those who aren't as familiar with components of any kind should find an improved development experience.