I am moving this gist to a github repo so more people can contribute to it. Also, it makes it easier for me to version control.
Please go to - https://github.com/praveenpuglia/shadow-dom-in-depth for latest version of this document.
Also, if you find the document useful, please shower your love, go ⭐️ it. :)
# Shadow DOM
> Heads Up! It's all about the V1 Spec.
In a nutshell, Shadow DOM enables local scoping for HTML & CSS.
> Shadow DOM fixes CSS and DOM. It introduces scoped styles to the web platform. Without tools or naming conventions, you can bundle CSS with markup, hide implementation details, and author self-contained components in vanilla JavaScript. - https://developers.google.com/web/fundamentals/getting-started/primers/shadowdom
It's like it's own little world which hardly affects or gets affected by the outside world.
It's what you write as a __component author__ to abstract away the implementation details of your component. It can also decide what to do with the user-provided __light DOM__.
# Terminologies
**- DOM :** What we get over the wire (or wireless :|) is a string of text. To render something on the screen, the browsers have to parse that string of text and convert it into a data model so it can understand things better. It also preserves the hierarchy from the original string by creating putting those parsed objects in a tree structure.
We need to do that to make the machines understand our documents better. This tree like data model of our document is called Document Object Model.
**- Component Author :** The person who creates a component and defines how it works. Generally the person who writes a lot of shadow DOM code. Example - Browser vendors create the `input` element.
**- Component User :** Well, they use components built by authors. They can pass in light DOM and set attributes and properties on your component. They can even extend the internals of a component if they want. Example - We the user who use the `input` element.
**- Shadow Root:** It's what gets attached to an element to give that element it's shadow DOM. Technically it's a non-element node, a special kind of _Document Fragment_.
```html
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ISOLATION
#shadow-root
...
____________________________________________ DOCUMENT FRAGMENT
```
Throughout the document, I have put shadow root inside those weird ASCII boundaries. This will put more emphasis on thinking how they are actually document fragments and they have a wall around them.
**- Shadow Host:** The element to which the shadow root gets attached. A host can access it's shadow root via a property on itself. `.shadowRoot`.
**- Shadow Tree :** All the elements that go into the Shadow Root, which is scoped from outside world, is called Shadow Tree.
> The elements in a shadow tree are __not__ descendants of the shadow host in general (including for the purposes of Selectors like the descendant combinator) - Spec
**- Light DOM:**
- The set of DOM elements we can sandwich between the opening and closing tags.
- The DOM that lives outside shadow DOM.
- The DOM, the _user_ of your element writes.
- The DOM that is the actual children of your element.
```html
^^^^^^^^^^^^^^^^^^^^^^^^^^^
#shadow-root
___________________________
A Nice Kitten!
```
**- DocumentFragment:**
> The DocumentFragment interface represents a minimal document object that has no parent. It is used as a light-weight version of Document to store a segment of a document structure comprised of nodes just like a standard document. The key difference is that because the document fragment isn't part of the actual DOM's structure, changes made to the fragment don't affect the document, cause reflow, or incur any performance impact that can occur when changes are made. - [MDN](https://developer.mozilla.org/en/docs/Web/API/DocumentFragment)
# How to create Shadow DOM?
```html
```
```js
let el = document.querySelector(".dom");
el.attachShadow({mode: "open"});
// Just like prototype & constructor bi-directional references, we have...
el.shadowRoot // the shadow root.
el.shadowRoot.host // the element itself.
// put something in shadow DOM
el.shadowRoot.innerHTML = "Hi I am shadowed!";
// Like any other normal DOM operation.
let hello = document.createElement("span");
hello.textContent = "Hi I am shadowed but wrapped in span";
el.shadowRoot.appendChild(hello);
```
## Off Topic Question - Could we use `append()` instead of `appendChild()` ?
Yes! But here are the differences from MDN.
- `ParentNode.append()` allows you to also append DOMString object, whereas `Node.appendChild()` only accepts Node objects.
- `ParentNode.append()` has no return value, whereas `Node.appendChild()` returns the appended Node object.
- `ParentNode.append()` can append several nodes and strings, whereas `Node.appendChild()` can only append one node.
## What happens if we use an `input` element instead of the div to attach the shadow DOM?
Well, it doesn't work. Because the browser already hosts it's own shadow DOM for those elements. Bunch of red colored english alphabets will be thrown at console's face. 😰
## Note
- Shadow DOM, cannot be removed once created. It can only be replaced with a new one.
- If you are creating a custom element, you should be creating the shadowRoot in it's constructor. It can also be probably called in `connectedCallback()` but I am not sure if that introduces performance problems or any other problems. 🤷♂️
- To see how browsers implement shadow DOM for elements like `input` or `textarea`, Go to `DevTools > Settings > Elements > [x] Show user agent shadow DOM`.
# Shadow DOM Modes
## Open
You saw the `{mode: "open"}` in the `attachShadow()` method right? Yeah! That's it. What open mode does is that it provides a way for us to access the shadow DOM of an element in JS via the element. It also lets us access the host element from within the shadow DOM.
This is done by the two implicit properties created when we call `attachShadow()` in `open` mode.
1. The element gets a property called `shadowRoot` which points to the shadow DOM being attached to it.
2. The `shadowRoot` gets a property called `host` pointing to the element itself.
```js
// From the "How to create shadow DOM" example
el.attachShadow({mode: "open"});
// Just like prototype & constructor bi-directional references, we have...
el.shadowRoot // the shadow root.
el.shadowRoot.host // the el itself.
```
## Closed
Pass `{mode: "closed"}` to `attachShadow()` to create a closed shadow DOM. It makes the shadow DOM inaccessible from JS.
```js
el.shadowRoot; // null
```
## So which one should I use?
Almost always use `open` mode shadow DOMs because they make it possible for both the component author and user to change things how they want.
Remember we did `el.shadowRoot` stuff up there? Yeah! That won't work with `closed` mode. The element doesn't get any reference to it's shadow DOM. This is especially a problem when creating custom elements and want to access the shadow DOM for manipulation.
```js
class CustomPicture extends HTMLElement {
constructor() {
this.attachShadow({mode: "open"}); // this.shadowRoot exists. add or remove stuff in there using this ref.
this.attachShadow({mode: "closed"}); // this.shadowRoot returns null. Bummer!
}
}
// You could always do the following in your constructor.
// but it totally defies the purpose of using closed mode.
this._shadowRoot = this.attachShadow({mode: "closed"});
```
Also, closed mode isn't a security mechanism. It just gives a fake sense of security. Nobody can stop someone from modifying how `Element.prototype.attachShadow()` works.
# Styles inside Shadow DOM.
- They are scoped.
- They don't leak out.
- They can have simple names.
- They are cool, be like them. 😎
```html
#shadow-root
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Hello!
__________________________________________________________________
```
## Oh! oh! So can we also include a stylesheet?
Yeeeeaaaah.. but not in all browsers. 😕
```html
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#shadow-root
Hello!
_____________________________________________________
```
## Does it get affected by global CSS?
Yeah. In some ways. Only the properties that are inherited will make their way through the shadow DOM boundary. Example -
- color
- background
- font-family etc.
The `*` selector also affects things because `*` means all elements and that includes the element to which you are attaching the shadow root too, the host element. Things which get applied to the host and can be inheried, will pass the shadow DOM boundary to apply to inner elements.
## Terminologies
- :host: targets the host element. BUT!
```css
/* winner */
custom-picture {
background: red;
}
/* loser */
#shadow-root
```
- :host(``): Does the component __host__ match the __selector__ ? Basically to target different states of the same host. Examples
```css
:host([disabled]){
...
}
:host(:focus){
...
}
:host(:focus) span {
/*change all spans inside the element when the host has focus*/
}
```
- :host-context(``): Is the __host__ a descendant of __selector__ ? Lets us change component's styles based on how the parent looks like. General application could be in theming.
```css
:host-context(.light-theme) {
background: lightgray;
}
:host-context(.dark-theme) {
background: darkgray;
}
/*You can also do...*/
:host-context(.aqua-theme) > * {
color: aqua; /* lame */
}
```
## A note about :host() and :host-context()
Both of these functional pseudo classes can only take `` but not a combinator like space or ">" or "+" etc.
In case of `:host()` that means you can only choose the attributes and other aspects of the host element.
In case of `:host-context()` that means you can choose the attributes and other aspects of one particular element which is an ancestor of the `:host`. Only one!
## Do the modes affect how styles get applied/cascaded?
Nope! Only affects how we do things in JS.
## How does the user-agent styles get applied to even the shadow DOM elements?
Based on how shadow Roots or in general DocumentFragment works, user-agent styles (global) shouldn't have applied to all the elements inside a shadow root. So how do they work?
From the spec...
> `Window`s gain a private slot `[[defaultElementStylesMap]]` which is a map of local names to stylesheets. This makes it possible to write elements inside shadow root and still get the default browser styles applied to them.
## Which styles to put in shadow DOM ?
The purpose of having styles inside a shadow DOM is to just have default styles and provide hooks via css custom properties so component users can make changes to those default styles.
```html
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
#shadow-root