Shadow DOM: encapsulation for semantic markup

There was a post on CSS-Tricks recently about SVG icons vs font icons. One of the criteria for judgement was semantics (is an or tag better than a inline symbols or :before pseudo class?) Another post on the same site discussed events and targets with inline SVG.

For me, both the issue of semantics and difficulties of handling complex DOM (i.e. the event target problem) are part of the broader topic of encapsulation. That is, you (probably) don’t really want to use SVG to create a button, you just want a button that looks like a filing cabinet icon. Let me expand upon that:

If you want a button that allows the user to select a file the obvious starting point would be a form input:

file type input

file type input, rendered in Chrome

This has a number of merits:

  • It’s simple
  • It’s semantically very accurate, as it allows input of type file
  • The browser does the hard work

The last point is particularly important; as is, with no JavaScript, the browser will render a button, when a user clicks the button a file-system explorer will open and when an enclosing form is posted the reference to the file will be set correctly.

So, now you decide you want to use an icon (this oneĀ instead of the boring “Choose file” button. How might one go about that? Well, it’s not as simple as adding an image inside the input tag: inputs are void elements so cannot have children. You could just use an img tag and maybe give it a sensible class:

That would look fine but you would definitely have less semantic code and you would need to implement all of the file input functionality yourself (or hack it).

There is a better solution (at least there will be when support improves): Shadow DOM. Shadow DOM, for those who don’t know, allows encapsulation through “functional boundaries in a document tree”. What does that mean? Well, in our example, it would allow us to encapsulate the DOM tree for the nice looking SVG icon (and other stuff if we wanted) as a kind of widget which we can inject into our page under another element – in this case an input – without changing the functionality or semantics of that element.

Consider this simple case:

var button = document.querySelector('input[type="file"]');
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = "<div>Hello from the other side</div>";

See the Pen gHEei by chrismichaelscott (@chrismichaelscott) on CodePen.

It might, upon first glance, appear to be a long winded way of changing some HTML but that isn’t what has happened. If you were to inspect element the input would still be an input, no sign of a div at all. More importantly, if you click on the message a file-system explorer dialogue opens; if you post a form the attachment is set. So what happened, then?

The shadowDom variable in the JavaScript is the point of separation, the “functional boundary”. From the outside, you still have an input, you’ve not changed that at all. However, because we set the inner HTML of the shadow root the browser knows that there is more underneath, and renders that content.

Let’s go back to the icon we want to use for our file input. We can now use the shadow DOM to make a file input render like an image:

var button = document.querySelector('input[type="file"]');
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = "<img src="" alt="Select file"></img>";

See the Pen jiqoh by chrismichaelscott (@chrismichaelscott) on CodePen.

Pretty cool, but we can make this much nicer to look at – both from a user’s perspective and the code. Remember that Shadow DOM is a form of encapsulation: so CSS defined for the document cannot affect the encapsulated tree (this is one of the main reasons for Shadow DOM but a different discussion). So how can one style the Shadow DOM? HTML5 allows us to define template elements. These elements are not rendered but the parts of the DOM tree under them can be used as templates. If we define a template we can include a style tag within it. Then, the entire content of the template can be used to define the Shadow DOM. Also, for cleanliness, the template can be defined in the head and kept out of the way:

var buttons = document.querySelectorAll('input[type="file"]');
var fileButtonTemplate = document.getElementById("file-button-template");
for (var x = 0; x < buttons.length; x++) {
  var shadowRoot = buttons[x].webkitCreateShadowRoot();

See the Pen jghes by chrismichaelscott (@chrismichaelscott) on CodePen.

So that’s it! A nice looking scalable vector icon which has all of the functionality of a native file input, is completely semantic and doesn’t feature transparent inputs absolutely positioned over styled content.

Graceful Degradation

Fortunately, Shadow DOM (used in this way, at least) degrades really nicely. The JavaScript could be improved to catch exceptions but functionally this should play well with older (not bleeding edge) browsers. That is, if the browser doesn’t support element.createShadowRoot() then nothing happens, the old file input is left as is. In fact, this is arguably better than using an SVG icon straight up as SVG isn’t supported by older browsers (IE 8 and backwards) and an SVG tag will not degrade nicely.

Leave a Reply

Your email address will not be published. Required fields are marked *