Using Encapsulation for Semantic Markup

Avatar of Chris Coyier
Chris Coyier on (Updated on )

The following is a guest article by Chris Scott. Chris takes us through a great use case for the Shadow DOM. As designers, we may want to style something in a certain way, but sometimes end up having to go to war with HTML, CSS, and JS to get it done. And even then, the results can be weighty, hacky, and unsemantic. Shadow DOM might be able to save us from that, giving us a fresh place to use whatever HTML we need (need 20 empty elements? No problem!) without exposing that mess to the actual DOM (which would suck for accessibility, semantics, etc.)

File inputs are notoriously hard to style. Let’s say you want to use an SVG icon instead of the default button-and-filename styling.

onjective

That is not a trivial styling change. Input elements are “no content” elements, i.e. an element that doesn’t have a closing tag. Thus, there is nowhere to put an SVG element “inside” of it. So how might you go about this?

Well, let’s take a progressive enhancement approach and start with the basic functionality:

<form>
  <input type="file"></input>
</form>

What do we get out of the box? Semantically, it’s very descriptive: there’s an element that allows the user to input a file. It’s also functional: if you click on it a file system dialogue opens.

Now let’s think about adding the icon. You could put the image near the input, perhaps using z-indexes and transparent inputs to retain the functionality of the input (like this). This approach is okay semantically (you have an input and an image) but it definitely feels hacky and is more difficult to test. Alternatively, you could use an <img> tag and drop the <input> altogether, but then you have all the issues of replicating the file input’s functionality and you have lost a bunch of meaning from the markup.

(There are other ways you could go about this as well. For instance a label-with-hidden-input, but for the sake of getting to the point here, let’s move on.)

Shadow DOM

A better approach, in my opinion, is using Shadow DOM. Shadow DOM is one of the ingredients to Web Components, and you can read up on all that here. I just want to talk about its semantic benefits, for now.

Shadow DOM is described in the W3C draft spec as functional encapsulation or functional boundaries. That means we can encapsulate some design and pin it to an existing node on the DOM tree (called the Shadow Root). Returning to our example:

var button = document.querySelector(&#x27;input[type=&quot;file&quot;]&#x27;);
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = &quot;&lt;div&gt;Hello from the other side&lt;/div&gt;&quot;;

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

Take a look at the result in the Pen above. This may look like a long-winded way of changing some HTML but that’s not actually what happened. If you were to “inspect element” in your browser you would still see the original file input – no sign of a div at all. More to the point, if you click on the string the browser will pop up a file system dialogue. This is the crux of encapsulation: from the outside the input is still an input but if you traverse the functional boundary then it’s not an input at all, it’s a string. That means that we can make an input (or any other tag, for that matter) render in any way want. We can set the inner HTML of the Shadow Root to be any valid HTML and it will render as such. Including, of course, adding SVG.

var button = document.querySelector(&#x27;input[type=&quot;file&quot;]&#x27;);
var shadowDom = button.webkitCreateShadowRoot();
shadowDom.innerHTML = &quot;&lt;img src=\&quot;https://s3-eu-west-1.amazonaws.com/chrisscott/codepen/iconmonstr-archive-7-icon.svg\&quot; alt=\&quot;Select file\&quot;&gt;&lt;/img&gt;&quot;;

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

Clearly that isn’t a particularly good looking alternative, but it’s a start. And the HTML is simple and semantically correct. To take this further let’s look at another tool in the HTML5 Web Components toolbox that’s useful for this kind of thing: template tags.

Templates

Template tags are basically a way of building HTML that is not rendered. The cool thing about templates is that you can use JavaScript to inject the DOM from a template into an element elsewhere in the document. In this case, we can define a template for our fancy vector image file input and use that to define the Shadow DOM for actual input tags. Here’s a suitable template:

<template id="file-button-template">
  <style>
    img {
      padding: 6px;
      border: 1px solid grey;
      border-radius: 4px;
      box-shadow: 1px 1px 4px grey;
      background-color: lightgrey;
      width: 30px;
      cursor: pointer;
    }
    img:hover {
      background-color: whitesmoke;
    }
  </style>
  <img src="https://s3-eu-west-1.amazonaws.com/chrisscott/codepen/iconmonstr-archive-7-icon.svg" alt="Select file"></img>
</template>

Bringing it all together

The template element can be placed into the head of a document, and I think that’s a nice place for it. The following Pen has the template we just defined in the head. If you open it on CodePen.io you can see the template by clicking the cog in the top left of the HTML panel.

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

Pretty good, I reckon. Semantically, it’s exactly the same as the vanilla HTML but it is visually completely bespoke.

Browser support, polyfills and graceful degradation

There’s some bad news (then more bad news and then a little, tiny glimmer of good news) in terms of support for Shadow DOM in browsers.

The first piece of bad news is that editing Shadow DOM is, currently, only supported in WebKit/Blink browsers, using the prefixed element.webkitCreateShadowRoot() method. Firefox 30 should support Shadow DOM as well, though.

Bad news number two: Polyfils may not support this use case – certainly Polymer doesn’t. In the case of the Polymer platform, to understand why the polyfill doesn’t behave as the native implementation does one needs to understand the mechanics of the polyfill. Essentially, the Polymer platform performs it’s encapsulation by replacing native DOM nodes with wrappers. These wrappers emulate the behaviour of actual DOM nodes; for example, you can set the innerHTML of the wrapper as you would a native node. The wrappers maintain the so-called light DOM and shadow DOM. In terms of encapsulation, this works fairly well as the wrapper can intercept calls on the light side (such as the children getter) and hide the shadow DOM.

var light = document.querySelector(&quot;div&quot;);
var template = document.querySelector(&quot;template&quot;);

light.createShadowRoot().innerHTML = template.innerHTML;

document.querySelector(&quot;code&quot;).innerHTML = 
  light.innerHTML

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

In this example, the retieved inner HTML of the “light” DOM show the encapsulation working (there is no h1 tag). However, the polyfill has not used actual Shadow DOM (otherwise it wouldn’t be a polyfill!) and you can verify that if you inspect the first div from the results. In the DOM, you will see, there is an h1 tag – not hidden in the Shadow DOM, just there. That’s because, underneath the wrapper, the Polymer platform is just manipulating the regular DOM tree as there is no shadow root.

In terms of the file input SVG icon that is not good. If one were to run the demo of the file input with the Polymer polyfill, as with the previous example, it would merely insert the img tag as a child of the input. If you recall, that is not allowed. An input is a void element and has no permitted child elements, so the browser will just ignore the img and style elements as invalid children. Here is a fork of the ‘pen illustrating the issue; again, inspect the element and you can verify that the polyfill has added the template as children of the input.

And so, to the promised “tiny glimmer of good news”… Even though support is limited at the moment, I still think that it is justifiable to start using Shadow DOM to do things like this, now. Why? If your user’s browser doesn’t support Shadow DOM it will just render a normal file input. That’s not a bad thing and considering the complex (read: hacky) alternatives I think Shadow DOM should be considered a plausible choice.

Notes on Shadow DOM, Web Components, etc

There are a huge number of use-cases for Shadow DOM and the related Web Components. This article has completely overlooked the advantages of encapsulation in terms of developing frameworks and widgets. For those interested it’s worth reading the specs and getting to grips with Polymer.