Shadow DOM: `open` vs `closed`
The Web Component spec is made up of four pillars: HTML Templates, Custom elements, Shadow DOM, and HTML Imports, although that last one hasn’t gotten traction among the standards committees at all.
As of v1 of the spec, the current version, the API to create a ShadowRoot
changed to add a configuration object that determines the encapsulation mode
that shadow tree will have upon creation. The ShadowRoot
created by v0’s createShadowRoot
can be considered open
; closed
is new to v1.
///// v0 API /////const e = document.createElement('div');
const shadowRoot = e.createShadowRoot();///// v1 API /////const e = document.createElement('div');
const shadowRoot = e.attachShadow({ mode: 'open' });// or for a closed Shadow DOM
const e = document.createElement('div');
const shadowRoot = e.attachShadow({ mode: 'closed' });
While plenty is generally said and written about the Shadow DOM spec overall, this encapsulation mode
is not commented on much, so what does it get us?
TL;DR
The short answer is… not a ton!
While there are certainly differences, when in doubt, go open
. Opting for closed
will mostly limit public access to the component’s shadow tree; however, that could come at the cost of flexibility for your component’s end users. If you don’t have a strong reason for it, go open
.
Closed mode
Let’s dive deeper into what closed
mode means for your component design.
If you’ve ever opted-in to see the user-agent’s Shadow DOM, perhaps you’ve seen the underlying shadow tree within a <video>
element. You may notice, however, that there’s no Javascript access to that shadow tree. That is essentially what closed
mode controls: access.
The design goal of Shadow DOM’s closed
mode is to “disallow” public access to the nodes within a closed shadow tree, and because encapsulation is one of Shadow DOM’s main goals, a closed shadow tree in many ways makes sense. However, as mentioned above, you don’t actually get a ton.
Let’s look at a demo, and I’ll explain further below.
In the demo above, both components showcases—demonstrating open
and closed
, respectively— are intentionally created to be almost identical in their underlying structure to the point that their one and only difference is their encapsulation mode. Notice how for all intents an purposes you’re largely getting the same Shadow DOM benefits: an isolated DOM, scoped CSS, and a declarative, markup-based API.
What changes is how we handle the ShadowRoot
upon creation:
class MyComponent extends HTMLElement {
constructor () {
super();
// With closed, a ShadowRoot reference needs to be stored
this._shadowRoot = this.attachShadow({ mode: 'closed' });
}
connectedCallback() {
// Do stuff with ShadowRoot via stored reference
}
}// Define component in custom element registry
window.customElements.define('my-component', MyComponent);// Create component
const el = document.createElement('my-component');
document.body.appendChild(el);// ShadowRoot is not publicly available
console.log(el.shadowRoot); // => null
Because Element.shadowRoot
’s public access is disallowed, it’s commonly perceived as a “security” feature. However, because it requires a stored reference, it’s not actually all that secure.
// Continuing the above example
console.log(el._shadowRoot); // => ShadowRoot {}
The alternative perhaps would be to store the ShadowRoot
references in a WeakMap
; however, that would not prevent an attacker from overriding Element.prototype.attachShadow
.
“If I can find ways to access the `ShadowRoot` anyway, then why not jut use `closed`?”
Glad you asked.
Ultimately, it’s up to you. However, keep in mind that closed
mode will make your components less flexible for end users. Not providing this access might result in users forking, creating their own, or simply finding another component that perhaps better suits their needs.
One last note
There are a couple other APIs that are impacted by closed
mode to be mindful of:
HTMLElement.assignedSlot
will always returnnull
.Event.composedPath()
will not include nodes in shadow trees.
Those are arguably not super common APIs as far as Shadow DOM goes, but it’s definitely worth noting regardless. The documentation pages linked to above contain examples of those respective cases.