The Dark Shadow of The DOM

Tweet

Shadow DOM is part of the Web Components specification, and is designed to address the encapsulation problems that plague some kinds of web development.

You know the kind of thing — if you build a custom widget, how do you avoid naming conflicts with other content on the same page? Most significantly, how do you prevent the page’s CSS from affecting your widget?

It’s easy if you control the whole page, but that’s often not the case — not if you’re making widgets for other people to use. The problem there is that you have no idea what other CSS will be present. You can certainly reduce the likelihood of such problems, by defining all your selectors as descendants from something with high specificity:

#mywidget > .mywidget-container
{
}
#mywidget > .mywidget-container > .mywidget-inner
{
}

But that’s only effective until the site defines a rule with two ID selectors. Or maybe you could use two, but then three come along!

Recently, I’ve been toying with the idea of defining dynamic selectors — the widget script traverses up the DOM and makes a note of every element ID between itself and the document root, then compiles selectors that include all those IDs.

But even that’s not guaranteed. There’s really nothing we can do to entirely prevent this problem, except to use an <iframe>, but that’s not a good solution — iframes limit the size and shape of the widget, they make an additional server request, and they create a keyboard trap in some browsers (e.g. Opera 12, in which you can’t Tab out of an iframe once you’ve Tabbed into it). So for all those reasons, iframes are best avoided.

Into The Shadow

The Shadow DOM aims to solve this issue. I won’t going into the details of how it works and how to use it (there are other articles that do that), but for the purposes of this article I’ll summarize it like this — the Shadow DOM encapsulates content by creating document fragments. Effectively, the content of a Shadow DOM is a different document, which is merged with the main document to create the overall rendered output.

In fact some browsers already use this to render some of their native widgets. If you open the Developer Tools in Chrome, select Show Shadow DOM from the settings panel (the cog icon bottom-right) and then inspect a "range" input, you’ll see something like this:

<input type="range">
  #document-fragment
    <div>
      <div pseudo="-webkit-slider-runnable-track">
        <div></div>
      </div>
    </div>
</input>

But you can’t get to those elements through the DOM, because they’re hidden from it:

alert(input.firstChild);		//alerts null

The shadow content is roughly analogous to an iframe document on a different domain — the DOM can see the iframe, but can’t see anything inside it.

So because it’s isolated, users can’t accidentally break it, there’s no possibility of naming conflicts with any classes or IDs you use, and the CSS on the main page won’t affect it at all.

Sounds brilliant, doesn’t it?

Into The Darkness

But hang on … if all that content is not in the DOM, then doesn’t that mean it’s not exposed to accessibility APIs either?

Yes, that’s exactly what it means.

Anything you put in a Shadow DOM is inaccessible to browser-based access technologies, such as screenreaders. It’s not available to search-engines either, but that’s always the case with scripted content. However screenreaders are different — they are script-capable devices — and so they do have access to scripted content.

But not this content!

Of course the specification is not ignorant of this division. In essence, it assumes a distinction between elements that contain text-content or informational attributes, and those that are simply empty boxes to create visual parts, like the "range" input’s thumb. Let’s refer to these as content elements and utility elements.

So how often do widgets have such a clear distinction between the two? For the "range" input example it’s obvious, but are all sliders built that way? I wrote a slider widget recently, for an accessible video player, and its markup looked like this:

<label for="slider-thumb">
  <button type="button" id="slider-thumb" 
    role="slider" aria-orientation="horizontal"
    aria-valuemin="0" aria-valuemax="120" 
    aria-valuenow="75" aria-valuetext="Time: 01:15">
    <span></span>
  </button>
</label>

The only part of that slider which could be put inside a Shadow DOM, is the <span> inside the <button>. The <button> itself is important content, with ARIA attributes that provide dynamic information to screenreaders and other access technologies.

To make that work with Shadow DOM we’d have to move all the ARIA attributes to the outer <label>, give it tabindex, and then use Shadow DOM for the inner elements. But that would be less accessible because we’d lose native semantics (e.g. the label’s for attribute no longer makes a valid association), and it would be less useful because it means the widget can’t submit any form data (so we’d need a separate form control, such as a hidden input).

But even if that were fine — and even if every widget we make has a clear and easy distinction between content and utility elements — the content part of the widget is still not encapsulated; it’s still vulnerable to naming-conflicts and unwanted CSS inheritance.

And we all know that some people won’t understand or respect that distinction anyway. People will use Shadow DOM for content, and use it to produce a whole new generation of inaccessible web applications.

I read a number of other articles about Shadow DOM in researching this one, and they all do the same thing — they all stop to make the point that you shouldn’t put content in a Shadow DOM, and then immediately afterwards they say, but let’s not worry about that.

Brilliant! A whole group of users dismissed in one idle caveat!

But let’s be kinder, hey. Let’s say that article examples can’t be judged in those terms. Let’s assume that everybody who uses Shadow DOM will do so with appropriate consideration, making sure they only use it for utility elements, not for content.

With that requirement, Shadow DOM only provides half a solution; and half a solution is no solution at all.

Into The Light

It seems to me that the entire concept of Shadow DOM is wrong. It’s an over-engineered approach that doesn’t really solve the problem, and any approach that uses document fragments will have the same flaw — as long as it’s necessary to differentiate between accessible and non-accessible elements.

What we really need is the conceptual opposite — a way of defining style-encapsulated subtrees which are still part of the document.

In other words, rather than having multiple documents that only the browser can traverse, we have a single document that only the browser treats as multiple documents.

This could be expressed with a simple element attribute:

<div encapsulated="encapsulated">

The HTML DOM would interpret that no differently — it’s just an element with a non-rendered attribute, same as any other. However the CSS DOM would interpret it as a kind of document fragment, effectively saying that the element and everything inside it does not inherit from higher scopes.

And we can already do the opposite — scoping styles to a subtree — either by using descendant selectors, or if you really must, using <style scoped> (although personally I’d avoid that until it’s available as a <link> attribute, because <style> elements undermine the separation of content and presentation).

To go with that encapsulated attribute, we could still use a better way to manage and template utility elements, but HTML is the wrong place to do that. Really, we shouldn’t have to define empty elements at all — they’re a functional necessity only because we have no other way of defining presentational subtrees — so that capability should be added to CSS.

In other words, it should be possible for a single element to define any number of pseudo-elements, and for pseudo-elements themselves to also define pseudo-elements. Something like this:

#mywidget::after
{
}
#mywidget::after + ::element
{
}
#mywidget::after > ::element
{
}
#mywidget::after > ::element + ::element
{
}

Which would create a virtual subtree like this:

<div id="mywidget" encapsulated="encapsulated">
  Text content
  <after>
    <element></element>
    <element></element>
  </after>
  <element></element>
</div>

Defining that stuff in CSS would imply a clear and innate distinction, that no developer could fail to understand — content goes in HTML, presentation in CSS, just the way it should be.

It remains to be seen whether we’ll ever get anything like what I’m suggesting. But in the meantime, I can only urge you to remember the absolute distinction — don’t use Shadow DOM for anything except empty elements which convey no information. And if you want my best suggestion, don’t bother with it at all.

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

  • Anonymous

    Interesting view point. I had wondered why the WebKit project was dropping more and more Shadow DOM code since the Blink fork.

    My main issue with this is that Web Components are extremely cool. It would be nice if we could solve these accessibility issues whilst still offering all the same encapsulation benefits…

  • Eric Bidelman

    There’s a common misconception that content in Shadow DOM is not accessible.

    Steve Faulkner had a nice writeup (http://blog.paciellogroup.com/2012/07/notes-on-web-components-aria/) on this topic a while back and came away pretty happy.

    Most assistive technologies hook directly into the browsers rendering tree, so they just see the fully composed tree. As you’ve pointed out, if you examine one of the native HTML elements that use Shadow DOM, for example, you’ll notice aria attributes inside the tree. These work great with assistive technology today! Other types of assistive tools like Chromevox will need to be updated to learn how to traverse the Shadow DOM. There’s on going work to make that happen.

  • Dimitri Glazkov

    Hi James!

    It’s great that you’re digging into Shadow DOM and trying to understand the implications of this new technology.

    To help you a little bit with this. Today, there are roughly two categories of assistive technologies: agents that use built-in browser/OS accessibility APIs and agents that use standard DOM APIs.

    The agents in the former category (JAWS, VoiceOver, etc.) effectively use the CSS box tree as their model, and the agents in the latter category use the DOM tree. When designing Shadow DOM, we made sure both categories are able to do their job.

    In case of the JAWS-like AT agents, the document and shadow trees are composed (http://www.w3.org/TR/shadow-dom/#composition) into one tree at the time of rendering, which means that these agents simply don’t have to know about shadow trees — it’s all one big happy tree from their perspective. Steve Faulkner did a nice exploration of this over a year ago: http://blog.paciellogroup.com/2012/07/notes-on-web-components-aria/

    Similarly, the DOM API-based AT agents still can access Shadow DOM. With Element.shadowRoot (http://www.w3.org/TR/shadow-dom/#api-partial-element-shadow-root), HTMLContentElement.getDistributedNodes(), and ShadowRoot.olderShadowRoot, these agents can traverse the exact composed tree that is currently being presented to the user.

  • Scott Miles

    Fwiw, the Shadow DOM can in fact be traversed. Any node with Shadow DOM has a `shadowRoot` property that points it’s shadow document. There is ongoing discussion with accessibility experts and SEO folks with how best to integrate Shadow DOM technology with screen readers and search engines. I think you may have mistaken “doesn’t do a thing” for “will never do a thing”. Encapsulation is fundamentally good when approaching a complex project. IMO, the one massive global DOM tree is one of today’s limiting factors for web development.

  • brothercake

    Yeah I kind of assumed that problems interpreting Shadow DOMs would be temporary. I am aware that there’s more than one kind of accessibility API, and that only DOM-based ones will have this problem. But the fact is, right now they do have this problem.

    I have no doubt that in the future this problem will be solved. But it isn’t solved now, so now is not the time to use Shadow DOM.

    But taking that as read — assuming that Shadow DOM content becomes accessible — you still can’t use Shadow DOM to solve the encapsulation problem, because it only applies to content which is generated through scripting.

    How do you encapsulate content that’s defined in static HTML?

  • brothercake

    I feel like I should post again to clarify the conclusions reached in this article and its subsequent comments.

    As far as browser-based screenreaders like JAWS are concerned, my conclusions were incorrect — Shadow DOM content is not inaccessible to these users (as long as they’re using a browser which supports it, but that’s an obvious given).

    As far as DOM-based access technologies (like browser extensions) are concerned, the inaccessibility of Shadow DOM content is temporary. Shadow DOMs are traversable, as long as the tools are updated to identify them and use the correct references.

    But I stand by my secondary conclusion, that Shadow DOM does not really solve the encapsulation problem, because it only applies to content which is generated through scripting.

  • Eric Bidelman

    The way you create Shadow DOM is through script, but it can easily be created from markup that’s not generated by script:
    http://www.html5rocks.com/en/tutorials/webcomponents/customelements/#fromtemplate

    • brothercake

      Okay, but can it be encapsulated without using scripting at all? If you’re building a widget which enhances static markup, then what happens to the static markup when scripting isn’t available?

      • Eric Bidelman

        Correct. Without script, you can’t create Shadow DOM from the content, and therefore get its encapsulation goodness. But this is a different topic.

        > what happens to the static markup when scripting isn’t available?

        It stays put :) It’s one reason for creating Shadow DOM from a

  • Scott Miles

    With a bit a JS library glue it’s easy to write static HTML that is automatically transplanted into ShadowDOM. Combine with Custom Elements, and you have a powerful encapsulation solution.Take a look at what is going on here (http://www.polymer-project.org/).

  • brothercake

    Your first answer is what I was asking.

    I understand how the template tag works and how the rules of HTML means that its content is treated no differently when it’s not handled by scripting (same as fallback content in an object element).

    What I’m trying to get at is whether that content is encapsulated. And you’ve confirmed what I thought — that it isn’t — and that’s my point. If there’s no way of encapsulating static content without scripting, then the encapsulation problem isn’t solved.

  • Eric Bidelman

    Technically, the static content in HTML templates is not the same as fallback content. If the browser understands

    • brothercake

      Unfortunately turning script off is anything but moot. There may be some application contexts where it’s reasonable to generate all content with scripting, to the extent that viewing the page without scripting is completely blank, but for the vast majority of the web that’s not the case. As long as we use static HTML, then its encapsulation will be an issue.

      I know that there’s debate as to whether turning JS off amounts to an accessibility issue. I think it does, but it’s really important what I think, it’s what clients ask for that matters. And clients still ask for stuff that works to some extent without JS. Often not the same extent, but some extent, and that means that the encapsulation of content without scripting remains an issue.

      I originally wrote this article because I was looking for a CSS encapsulation solution, and I was disappointed by what I discovered with Shadow DOM. Clearly my accessibility concerns were partly wrong, and that’s encouraging, but the rest of my concerns with Shadow DOM remains.

      What we have with Shadow DOM is a technique with many potential advantages, one of which is encapsulation. That’s great, but in and of itself is not a solution to the encapsulation problem.

      A proper solution to the encapsulation problem would not have any reliance on scripting, nor would it change the traversal model, or indeed have anything to do with HTML or the DOM. It would simply be logic that CSS parsers use to decide which rules apply to the document and which should be discarded.

  • James Edwards

    Something new caught my eye recently with regard to encapsulation failures.

    Putting aside the issue of noscript support, there are still conditions in which shadow DOM fails to provide encapsulation. In Steve Faulkner’s article discussing whether Shadow DOM has an impact on ARIA ( http://blog.paciellogroup.com/2012/07/notes-on-web-components-aria/ ) it’s noted how Shadow DOM creates an encapsulation boundary that avoids ID collisions — i.e. an element in a Shadow DOM can have the same ID as an element in the main DOM, without collision.

    But what this boundary means is that attribute associations can’t be made across it — for example, a LABEL element in the main DOM can’t refer to the ID of an element in the Shadow DOM, or an aria-labelledby attribute in the Shadow DOM can’t refer to an ID in the main DOM.

    So if you’re building a custom widget, you MUST have at least part of that widget in the main DOM — such as the INPUT element container for the Shadow DOM components of a slider control — otherwise it won’t be possible to associate the widget with a label or caption.

    Therefore, that part of the widget is not encapsulated.