This document is my attempt to track the difference between Shadow DOM v0 and v1.
This is not a tutorial for Shadow DOM. Rather, this is my attempt to provide a guide for those who are already familiar with Shadow DOM v0 and want to migrate their components to v1. This guide should be considered work-in-progress. I will make my best efforts to maintain this guide.
Creating a shadow root
v0
Use Element.createShadowRoot().
let e = document.createElement('div');
let shadowRoot = e.createShadowRoot();
v1
Use Element.attachShadow({ mode: 'open' }) for an open shadow root.
let e = document.createElement('div');
let shadowRoot = e.attachShadow({ mode: 'open' });
Use Element.attachShadow({ mode: 'closed' }) for a closed shadow root.
let e = document.createElement('div');
let shadowRoot = e.attachShadow({ mode: 'closed' });
mode is mandatory in v1.
let e = document.createElement('div');
// let shadowRoot = e.attachShadow(); // Throws an exception because `mode` is not given.
Multiple Shadow Roots
v0
Supported.
let e = document.createElement('div');
let olderShadowRoot = e.createShadowRoot();
let youngerShadowRoot = e.createShadowRoot(); // It's okay. A shadow host can host more than one shadow roots.
v1
No longer supported.
let e = document.createElement('div');
let shadowRoot = e.attachShadow({ mode: 'open' });
// let another = e.attachShadow({ mode: 'open' }); // Error.
A closed shadow root
v0
A shadow root is always open.
v1
v1 has a new kind of a shadow root, called closed.
The design goal of a closed mode is to disallow any access to a node in a closed shadow root from an outside world.
It is similar that a user's JavaScript can never access an inside of a <video> element in Google chrome.
A <video> element is using a closed-mode shadow root in its implementation in Blink.
Open:
let e = document.createElement('div');
let shadowRoot = e.attachShadow({ mode: 'open' });
console.assert(e.shadowRoot == shadowRoot); // It's okay. shadowHost.shadowRoot returns a shadow root if it is open.
Closed:
let e = document.createElement('div');
let shadowRoot = e.attachShadow({mode: 'closed'});
console.assert(e.shadowRoot == null); // shadowHost.shadowRoot does not return the shadow root if it is closed.
The following APIs are subject to this kind of constraints:
- Element.shadowRoot
- Element.assignedSlot
- TextNode.assignedSlot
- Event.composedPath()
Element.prototype.attachShadow from being hijacked.
Elements which can be a shadow host
v0
Every element can be a shadow host, theoretically.
let shadowRoot1 = document.createElement('div').createShadowRoot();
let shadowRoot2 = document.createElement('input').createShadowRoot(); // Should be okay.
v1
A limited number of elements can be a shadow host.
let shadowRoot = document.createElement('div').attachShadow({ mode: 'open' });
// document.createElement('input').attachShadow({ mode: 'open' }); // Error. `<input>` can not be a shadow host.
See the definition of the attachShadow for the complete list of such elements. Custom elements can be a shadow host.
Insertion Points (v0) vs Slots (v1)
v0
Use <content select=query> to select host's children. It can select host's children by CSS query selector.
<!-- Top level HTML -->
<my-host>
<my-child id=c1 class=foo></my-child>
<my-child id=c2></my-child>
<my-child id=c3></my-child>
</my-host>
<!-- <my-host>'s shadow tree -->
<div>
<content id=i1 select=".foo"></content>
<content id=i2 select="my-child"></content>
<content id=i3></content>
</div>
The result is:
| Insertion point | Distributed nodes |
|---|---|
| #i1 | #c1 |
| #i2 | #c2, #c3 |
| #i3 | Empty |
The v0 also had <shadow> insertion points, however, let me skip the explanation of <shadow> because multiple shadow roots are deprecated.
v1
Use <slot> to select host's children. It selects host's children by exact slot name matching.
<!-- Top level HTML -->
<my-host>
<my-child id=c1 slot=s1></my-child>
<my-child id=c2 slot=s2></my-child>
<my-child id=c3></my-child>
</my-host>
<!-- <my-host>'s shadow tree: -->
<div>
<slot id=i1 name=s1></slot>
<slot id=i2 name=s2></slot>
<slot id=i3></slot>
</div>
The result is:
| Slot | Distributed nodes |
|---|---|
| #i1 | #c1 |
| #i2 | #c2 |
| #i3 (also known as the "default slot") | #c3 |
Re-distribution: Directly (v0) vs Indirectly by flattening (v1)
v0
<!-- Top level HTML -->
<my-host>
<my-child id=c1 class=foo></my-child>
<my-child id=c2></my-child>
<my-child id=c3></my-child>
</my-host>
<!-- <my-host>'s shadow tree -->
<my-splatoon>
<content id=i1 select=".foo"></content>
<my-child id=c4 class=foo></my-child>
<content id=i2 select="my-child"></content>
<content id=i3></content>
</my-splatoon>
<!-- <my-splatoon>'s shadow tree -->
<content id=j1 select="#c3"></content>
<content id=j2 select=".foo"></content>
<content id=j3></content>
The result is:
| Insertion point | Distributed nodes |
|---|---|
| #i1 | #c1 |
| #i2 | #c2, #c3 |
| #i3 | Empty |
| Insertion point | Distributed nodes |
|---|---|
| #j1 | #c3 |
| #j2 | #c1, #c4 |
| #j3 | #c2 |
v1
<!-- Top level HTML -->
<my-host>
<my-child id=c1 slot=s1></my-child>
<my-child id=c2 slot=s2></my-child>
<my-child id=c3></my-child>
</my-host>
<!-- <my-host>'s shadow tree -->
<my-splatoon>
<slot id=i1 name=s1 slot=j1></slot>
<slot id=i2 name=s2 slot=j1></slot>
<my-child id=c4 slot=j1></my-child>
<slot id=i3 slot=j3></slot>
</my-splatoon>
<!-- <my-splatoon>'s shadow tree -->
<slot id=j1 name=j1></slot>
<slot id=j2 name=j2></slot>
<slot id=j3 name=j3></slot>
The result is:
| Slot | Distributed nodes |
|---|---|
| #i1 | #c1 |
| #i2 | #c2 |
| #i3 | #c3 |
| Slot | Distributed nodes |
|---|---|
| #j1 | #c1, #c2, #c4 |
| #i2 | empty |
| #j3 | #c3 |
You can find another complex example in the Shadow DOM specification.
Fallback contents
v0
No supports.
v1
Child nodes of <slot> can be used as fallback contents.
A good analogy of this feature is "default value of function parameter" in a programming language.
The following example is borrowed from Blink's CL
<!-- Top-level HTML -->
<div id='host'>
<div id='child1' slot='slot2'></div>
</div>
<!-- #host's shadow tree -->
<slot name='slot1'>
<div id='fallback1'></div>
<slot name='slot2'>
<div id='fallback2'></div>
</slot>
</slot>
<slot name='slot3'>
<slot name='slot4'>
<div id='fallback3'></div>
</slot>
</slot>
The result is
| Slot | Assigned nodes | Distributed nodes |
|---|---|---|
| slot1 | empty | #fallback1, #child1 |
| slot2 | #child1 | #child1 |
| slot3 | empty | #fallback3 |
| slot4 | empty | #fallback3 |
Thus, the flat tree will be:
<div id='host'>
<div id='fallback1'></div>
<div id='child1'></div>
<div id='fallback3'></div>
</div>
Events to react the change of distributions
v0
No way.
v1
A v1 has a new kind of events, called slotchange. If a slot's distributed nodes changes as a result of DOM mutations, slotchange event will be fired at the end of a microtask.
HTML:
<!-- Top level HTML -->
<my-host>
<my-child id=c1 slot=s1></my-child>
</my-host>
<!-- <my-host>'s shadow tree -->
<slot id=i1 name=s1></slot>
JavaScript:
slot_i1.addEventListener('slotchange', (e) => {
console.log('fired');
});
let c2 = document.createElement('div');
my_host.appendChild(c2);
c2.setAttribute('slot', 's1');
// slotchange event will be fired on slot, '<slot id=i1 name=s1>', at the end of a micro task.
TODO(hayato): Explain this feature in-depth. For a while, see #issue 288 for the context.
Styling for distributed nodes
v0
Use ::content selector pseudo elements.
<!-- Top level HTML -->
<my-host>
<my-child id=c1 class=foo></my-child>
<my-child id=c2></my-child>
<my-child id=c3></my-child>
</my-host>
<!-- <my-host>'s shadow tree -->
<div>
<content id=i1 select="my-child"></content>
</div>
<style>
#i1::content .foo {
color: red;
}
</style>
#c1 becomes red.
v1
Use ::slotted (compound-selector) pseudo elements.
<!-- Top level HTML -->
<my-host>
<my-child id=c1 slot=s1 class=foo></my-child>
<my-child id=c2 slot=s1></my-child>
</my-host>
<!-- <my-host>'s shadow tree: -->
<div>
<slot id=i1 name=s1></slot>
</div>
<style>
#i1::slotted(.foo) {
color: red;
}
</style>
#c1 becomes red.
::content can take any arbitrary selector, ::slotted can only take a
compound selector (in the parenthesis).
The reason of this restriction is to make a shadow boundary crossing selector style-engine friendly, in tern of a performance.
In v0, it is difficult to avoid a performance penalty caused by arbitrary selector in a shadow boundary crossing selector.
Shadow piercing combinators
v0
Use /deep/ (zero-or-more shadow boundary crossing) and ::shadow (one level shadow boundary crossing).
v1
No alternative.
CSS Cascading order
v0
The spec has a bug and the implementation in Blink is broken. It's too late to fix it without breaking the Web.
v1
Clarified. In short: "An rule in an outer tree wins an rule in an inner tree".
/deep/ and ::shadow are unavailable in v1, only ::slotted is affected by the new rule, as of now.
See this document for the example.
Sequential Focus Navigation
v0
A document tree and a shadow tree are forming a scope of sequential focus navigation.
v1
In addition to v0, <slot> becomes a scope of sequential focus navigation.
See the comment in the spec issue for an example.
TODO(hayato): Explain the concept behind the scene and its behavior here.
DelegatesFocus
TODO(hayato): Explain this.
ActiveElement
TODO(hayato): Explain the difference. For a while, see webcomponents #358.
Events across shadow boundaries
v0
Events are propagating across shadow boundaries by default, except for a limited kinds of events. See the list.
v1
Events are scoped in a tree by default, except for some of UA UIEvents.
For user-made synthetic events, you can control the behavior by a composed flag.
HTML:
<!-- Top level HTML -->
<my-host></my-host>
<!-- <my-host>'s shadow tree -->
<div id=d1></div>
</style>
JavaScript:
my_host.addEventListener('my-click1', (e) => {
console.log('my-click1 is fired'); // This will not be called.
});
my_host.addEventListener('my-click2', (e) => {
console.log('my-click2 is fired'); // This will be called.
});
d1.dispatchEvent(new Event('my-click1', { bubbles: true }));
d1.dispatchEvent(new Event('my-click2', { bubbles: true, composed: true }));
At #my-host, only an event listener for my-click2 is called.
Getting Event path
v0
Use Event.path, which is a property.
v1
Use Event.composedPath(), which is a function.
Event.composedPath() returns an empty array, while Event.path does not.
Functions which are renamed
| V0 | V1 |
|---|---|
insertionPoint.getDistributedNodes() |
slot.assignedNodes({flatten: true}) |
| No equivalence | slot.assignedNodes() |
Element.getDestinationInsertionPoints() |
Element.assignedSlot (The meaning is slightly different. It returns only the directly assigned slot.) |
New utility functions in Node
These functions are just utility functions. Thus, v0 or v1 does not matter.
-
Returns true if the node is in a shadow-including document.
-
Returns its root.
It turned out thatNode.rootNodeis not a Web-compatible name. We have broken some Web sites. We are seeking an alternative name. See whatwg/dom #241 for details.
Questions?
If you find a typo, mistake or a question in this document, please file an issue here.
If you have a question about the Web Standard itself, please see the followings: