Web components became a real solution for creating reusable HTML custom elements when the shadow DOM was created.
The shadow DOM is used to isolate all the elements inside your web component from the outside document.
It is a separate island, free to behave however it wants, without needing anything from outside, free from breaking
something else on the page that happens to have the same id
value or class
name.
In a document (the web page) you have a tree like structure of elements. The root of the tree is known as the document DOM. You can create a web component that hosts its own DOM that is isolated from the rest of the document. The web component is a normal element, but it hosts a shadow DOM. Inside the shadow DOM you can add other elements, create events, and do almost everything you can do in the document DOM.
Each web component can have its own shadow DOM.
The same web component, used twice on a page, can create elements in their shadow DOM, that do not interferer
with each other or anything else in the document.
In the above diagram the web component creates an element with an id
.
The document DOM cannot have duplicate id
values, but each shadow DOM has their own id
with the
same value, but this is perfectly fine.
Creating a web component with a shadow DOM is very simple. Take a look at the example code below.
class ShadowDomExample extends HTMLElement { constructor() { super(); // Attach shadow DOM root this.attachShadow({ mode: 'open' }); // Set inner HTML this.shadowRoot.innerHTML = '<p><b>Shadom DOM Example</b></p><p>I exist in my own DOM.</p>'; } }
The best place to attach the shadow DOM is in the constructor function.
To create and attach a shadow DOM you need to call the element's attachShadow
function.
This function takes a single object parameter which controls how the shadow DOM is created and processed.
For now, we just set the mode value to 'open'
.
The function does return the shadow root object but it also sets the element's shadowRoot
member.
This is the root element of the shadow DOM, which is where we can add all our own internal elements to.
<!DOCTYPE html> <html lang="en"> <head>...</head> <body> <p>...</p> <shadow-dom-example> #shadow-dom (open) <p> <b>Shadow DOM Example</b> </p> <p>I exist in my own DOM.</p> </shadow-dom-example> </body> </html>
You will notice that the first sub-element in the web component is the #shadow-root
and that it is labelled as open.
Below this are the elements we added to the shadow DOM.
One of the benefits of using a shadow DOM is element isolation.
All the elements inside one has no affect on the elements in another.
This is best seen when using an element id
.
A document (web page) must only contain unique id
values.
Calling the function document.getElementById
could return the wrong element if the id
value was being used by multiple elements.
We can, however, use the same id
value within a shadow DOM, even if the web component it belongs to is used
multiple times on the web page.
The id
value still needs to be unique within a single shadow DOM.
The code below is used to setup our example.
constructor() { super(); // Attach shadow DOM root this.attachShadow({ mode: 'open' }); // Set inner HTML this.shadowRoot.innerHTML = '<p id="result">Number = ?</p><button id="button">Change Text</button>'; // Get result element this._resultElement = this.shadowRoot.getElementById('result'); // Get button element this._buttonElement = this.shadowRoot.getElementById('button'); }
We create and attach the shadow DOM. We then set the inner HTML elements, which contains a button and a paragraph for the result output. Pressing the button will create and show a random number.
There are two copies of the web component, and therefore the same id
is used in each one, but they are
isolated and do now affect each other.
Pressing the button that looks for the id="result"
, using the document.getElementById
function, will
show that they can not find the elements with that id
value, because it is isolated within the shadow DOM.
We do call a similar function, this.shadowRoot.getElementById
, but this looks for the element within the shadow DOM, not the document.
You can create and attach a shadow DOM as either open
or closed
, but what is the difference?
It is all linked to the host element's shadowRoot
property.
If the shadow DOM is open, then this property contains the shadow root object, but if the shadow
DOM is closed then the property is set to null, and therefore cannot be used.
But if it cannot be used then how do add your own elements inside?
The attachShadow
function not only creates and attaches the shadow DOM, but it also returns the shadow root object.
With this you can insert your own elements.
Take a look at the example code below.
class ClosedShadowDom extends HTMLElement { constructor() { super(); // Attach shadow DOM root (returns the shadow root object) const shadowRoot = this.attachShadow({ mode: 'closed' }); // Because the shadow DOM is closed the shadowRoot property is always null // therefore we need to use the returned shadowRoot variable instead // Set inner HTML shadowRoot.innerHTML = '<div>Closed shadow DOM</div>'; } }
Once the constructor has finished, there is no access to the shadow root object. One of the benefits to this is that outside JavaScript code cannot access any inner parts of our web component, they cannot access the internal elements of the shadow DOM. Lets see an example of this.
There are two web components, one using an open shadow DOM and the other using a closed one.
You can check to see if you have access to the shadowRoot
property for each one.
Even though the closed shadow DOM does not allow you to go inside the shadow DOM, you can see inside using the browsers debug element viewer. You can also make adjustments to styles and other parts.
<!DOCTYPE html> <html lang="en"> <head>...</head> <body> <p>...</p> <open-shadow-dom id="open"> #shadow-root (open) <div>Open shadow DOM</div> </open-shadow-dom> <closed-shadow-dom id="closed"> #shadow-root (closed) <div>Closed shadow DOM</div> </closed-shadow-dom> </body> </html>
Because of all this, having a closed shadow DOM does not seem to be of any real use, and it may be best to always create an open one.
Most standard elements do not allow a shadow DOM to be attached.
Any autonomous web component can, but if you are creating one derived from something other than HTMLElement
then it is unlikely you will be able to.
There are some exceptions however.
Below is a list of the tags you can do this with.
Tag | Class Name |
---|---|
<article> | HTMLElement |
<aside> | HTMLElement |
<blockquote> | HTMLElement |
<body> | HTMLBodyElement |
<div> | HTMLDivElement |
<footer> | HTMLElement |
<h1> to <h6> | HTMLHeadingElement |
<header> | HTMLElement |
<main> | HTMLElement |
<nav> | HTMLElement |
<p> | HTMLParagraphElement |
<section> | HTMLElement |
<span> | HTMLSpanElement |
Lets take a look at how this works and how it does not work.
The first web component, derived from the paragraph element, is able to attach its shadow DOM without any problems. The other one, derived from the anchor element, fails when it tries to attach a shadow DOM, giving you the following error message.