HTML Component Model & the Shadow DOM
It’s that time of the year, the time when one’s brain, stomach and liver are pushed beyond their operating limits. Yep, it’s conference season.
Last week I went to Fronteers and GOTO Aarhus. Both were great, but it’ll take something incredible to stop Fronteers being my favourite conference of the year.
Alex Russell was speaking at both, and covered (among other things) scoped styling and the HTML component model. Now, I don’t really care for scoped styling (see my mini rant on twitter), but the HTML component model has really caught my imagination.
Defining away magic & giving developers creativity
Allow me to slightly abuse a quote from Storm by Tim Minchin
Throughout history every mystery ever solved has turned out to be not magic
As platforms develop they tend to create magic as a side effect, then later the magic is removed and turned into toys. I’m serious, stop looking at me like that.
- This is a list item…
Back in the dark days, a list item would have a margin and a bullet point, this was created by the browser & there wasn’t a lot you could do about it. This was a problem if you wanted to express something as a list but didn’t want it to look like the browser wanted it to. The styling was magic.
Along came CSS and explained why elements look like they do, and gave us a brand new toy box, we could change the style of elements. Today we can inspect a list item & see the default styling a browser has given to that element, along with how our own styles override those defaults. Less magic! More toys!
There are still bits of magic hanging around that give us trouble, the <legend> element springs to mind, which I’ve avoided using even though it was a ‘best-fit’, because it’s too much of a cross-browser time-vampire to style.
Some elements have a lot of magic going on:

This is an <audio> element, or at least its implementation in Google Chrome. As you can see it has buttons with behaviours & a slider control. These are implemented as DOM elements in Chrome (and other browsers), but they have a magic invisibility cloak. We can’t access the buttons or slider control using document.querySelector or similar, they’re not visible in Web Inspector / Firebug. Also, they have a magic style forcefield:
audio * { border: 3px solid red; }
The above has no effect on the controls of the audio element. Magic!
Soon it won’t be magic, it’ll be toys.
HTML Component Model
Our saviour in this case, is the HTML Component Model spec. It’s very much a draft, there are no implementations in current browsers, and it may completely change by the time I’ve finished writing this sentence… thankfully it hasn’t yet… still the same… ok I think it’s safe to continue.
The component model turns the following bits of magic into toys:
- How a particular tag name is linked to a constructor and prototype
- How a single DOM-visible element appears to be made up of multiple parts
- How those parts are protected from some/all styles
Linking tags to constructors
Here’s how I’d create a modal dialog element:
// Creating a constructor function ModalDialog() { // We're inheriting from HTMLElement, so call its constructor HTMLElement.call( this ); } // Inherit from HTMLElement ModalDialog.prototype = Object.create( HTMLElement.prototype ); // Give the element a behaviour ModalDialog.prototype.helloWorld = function() { console.log( 'Hello world' ); }; // Apply it to this document Element.register( 'x-modal-dialog', ModalDialog );
Once the element is registered, any <x-modal-dialog> elements will be reinitialised as instances of ModalDialog.
// Logs 'Hello world', if there's a <x-modal-dialog> in the document document.querySelector( 'x-modal-dialog' ).helloWorld();
However, something like a modal dialog wouldn’t typically exist on the page already, it would be added via js. To add a new ModalDialog:
// Old familiar factory method document.body.appendChild( document.createElement('x-modal-dialog') ); // "Why didn't we have this before?" constructor method document.body.appendChild( new ModalDialog ); // From a string of markup document.body.innerHTML = '<x-modal-dialog>';
I’m unsure if the ‘x-’ prefix will be enforced, or simply convention.
The shadow DOM
The shadow DOM is seriously cool. The regular DOM is clean-shaven, does charity work & goes to bed early on a school night. The shadow DOM has a goatee, sits in a bar and tells you to go fuck yourself if you ask it for an autograph.
The shadow DOM is where you add elements that make up your component. In the <audio> example, the shadow DOM contains the buttons and the sliders
function ModalDialog() { HTMLElement.call( this ); // Make a close button var closeBtn = new HTMLButtonElement; closeBtn.innerHTML = 'Close'; // Give our dialog a shadow DOM var shadow = new ShadowRoot( this ); // Add the button to the shadow DOM shadow.appendChild( closeBtn ); }
Now our dialog has a close button which doesn’t appear in document.querySelectorAll('button'). Our close button doesn’t actually do anything when it’s clicked, it’s just a button, we’d add that behaviour with js.
The ShadowRoot is a similar to a document fragment, in that it acts more like a collection of elements than an element itself.
Adding children to your element
So far our component behaves like <img> and <input>, as in it cannot have child nodes. This is pretty silly for a dialog, as you’d expect to contain flow content, eg:
<x-modal-dialog> <h1>Keyboard not connected</h1> <p>Press F1 to continue</p> </x-modal-dialog>
Making this work is pretty simple:
function ModalDialog() { HTMLElement.call( this ); var closeBtn = new HTMLButtonElement; closeBtn.innerHTML = 'Close'; var shadow = new ShadowRoot( this ); shadow.appendChild( closeBtn ); // Add a content element to the shadow DOM shadow.appendChild( new HTMLContentElement ); }
Done! Anything inside our <x-modal-dialog> tag will be moved to the <content> element within the shadow DOM, which appears after our close button. This means you can have multiple elements around <content> that build up the interface of your component.
Everything in its right place
Currently we’re dumping all the content into one content element. That’s cool, but some elements might need to appear in special places. We see this with elements like <thead>, even if they appear after the <tbody> in markup, they’ll render above it.
This was magic. Now we have toys:
function ModalDialog() { HTMLElement.call( this ); var shadow = new ShadowRoot( this ); // Make a header for our dialog & add it to shadow DOM var header = document.createElement( 'header' ); shadow.appendChild( header ); // Add a content element to the shadow DOM var bodyContent = new HTMLContentElement; shadow.appendChild( bodyContent ); // Add another content element for header stuff var headerContent = new HTMLContentElement; header.appendChild( headerContent ); // Set it to receive h1 elements headerContent.select = 'h1'; }
The select property can be a series of space-separated simple selectors. Very cool. Surprisingly this includes pseudo-class selectors, meaning you could make something switch to another location in the shadow DOM on :focus or :checked, interesting.
Modifying the shadow DOM of existing elements
An element can only have one shadow DOM, if one is created for an element that already has a shadow DOM an exception is thrown.
I’m not a huge fan of this, but Alex proposed a better solution over beers: An element can only have one shadow DOM, if another is created the existing shadow DOM is removed.
This would allow us to do this:
var select = document.querySelector( 'select[name="country"]' ); var shadow = new ShadowRoot( select );
Now we’re able to recreate the look and feel of the <select> by creating our own shadow DOM. As long as we manage properties like value, the browser will include that value when the form is submitted.
This solution caters for browsers that may not already use a shadow DOM for a particular element. I’m pretty certain most browsers use a shadow DOM for a select element, remember the fuss we had when IE didn’t. However, mobile browsers tend to bring up a native bit of UI when the select element is interacted with. This is a good thing, browser vendors should be free to go the native or shadow DOM route, whatever gives the best user experience. However, if I were to create a new shadow DOM for a <select>, the browser should hand full control of the element to me, and not use any native UI trickery on interaction.
Won’t someone think of the semantics!
The great thing about this article being so unexpectedly long, is anyone who disagrees with allowing developers to extend HTML has long since punched a wall and stormed out of the room muttering obscenities. So, y'know, fuck those guys.
Oh ok. Let’s talk semantics.
In the same way you wouldn’t use a <div> when <article> is a better fit, don’t make your own component if something already does the job. But, you want it to look a bit different, either change the shadow DOM for individual instances, or consider inheritance.
In the previous example I changed the shadow DOM for an individual select element. For multiple select elements, perhaps I could do this:
function StyledSelect() { // We're going to inherit directly from select... HTMLSelectElement.call( this ); // Add a lovely looking shadow DOM here } // Take everything the select already has StyledSelect.prototype = Object.create( HTMLSelectElement.prototype ); // Register it Element.register( 'x-select', StyledSelect );
Now my <x-select> elements have the same semantics and interface as <select>, because it inherits from it. Its value property should be serialised when its parent form is submitted.
// Maybe even, I could do this... Element.register( 'select', StyledSelect );
This is all guesswork and hoping, as I mentioned earlier I don’t know if the ‘x-’ prefix is convention or enforced. I’d quite like to be able to change the behaviour of all selects in one swoop. I think I would… or would it be the end of the world? Hmm.
If you’re creating something which has no resemblance to any existing element, then you’re not losing any semantics, because there were none to begin with. However, you should make sure your shadow DOM children are using elements that best-fit what they do, and use WAI-ARIA to fill in any gaps for assistive technologies.
Styling your components
Oh yes, I keep mentioning styling don’t I… I’ll save that stuff for another article.