Introduction to Web Components — Part II Shadow Dom

Alex Korzhikov
ITNEXT
Published in
6 min readSep 18, 2019

--

The first part of Web Components overview is about the Custom Elements standard.

Shadow DOM

The Shadow DOM specifications bring a scope concept to Web styles’ definitions. Those scopes are implemented through multiple DOM trees, embedded into a single document. In some way, the Shadow DOM's approach can be compared with the iframe element inside a document, as it has a naturally-isolated DOM structure. In terms of documents, the Shadow DOM's definitions are spread between several different standards, like DOM specification, HTML specification, CSS Scoping Module, UI Events, etc.

Web Components

The DOM specification defines a model for node trees and how nodes communicate with each other through events. The DOM node tree is a hierarchical structure of nodes, where some nodes can branch out, while others are just leaves. The specification differentiates Document Tree and Shadow Tree via the connection to their roots. The document object is the root of the Document Tree, while for the Shadow Tree a special type of root is proposed which is called the Shadow Root. The Shadow Root must be attached to any other tree under the host element. The Shadow Root can be thought of as a Document Fragment - the specification defines a belonging relationship between two interfaces.

<script>
class CustomElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot.innerHTML = '<a href="#">My link</a>'
}
}
customElements.define('hello-shadow-dom', CustomElement)
</script>
<hello-shadow-dom></hello-shadow-dom>

The only way to create the Shadow DOM is to call the attachShadow() method of the Element class. It creates a shadow root for the element. Only one shadow root can be attached for the element. The mode property specified on the attachShadow() call defines whether a tree is accessible from JavaScript or not. The alternative to the 'open' value would be the 'closed' value. The attached shadowRoot property is a parent of the specified node tree. The shadowRoot.host is set up to the hello-shadow-dom element in the following example.

<hello-shadow-dom></hello-shadow-dom>
#shadow-root (open)
<a href="#">My link</a>

The Slot feature is widely used in UI Libraries. In Angular, it is called projection or transclusion and it is supported via the ng-content directive. Vue seems to be more compliant with standards - it calls the same functionality as the slot. Finally, in React that ability is even more germane to the library and can be achieved by {props.children} usage inside the element container. The Slot feature serves a placeholder purpose. On one hand, the Shadow Tree may contain slots elements defined with the slot tag. When a developer wants to use the element on the other hand, he or she can specify a markup for declared slots so the content will be rendered inside the Shadow Tree wrapper.

<script>
class OhMySlotElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot.innerHTML = `<p>
<slot name="my-text">default</slot>
</p>`
}
}
customElements.define('oh-my-slot', OhMySlotElement);
</script>
<oh-my-slot>
<pre slot="my-text">
anything
</pre>
</oh-my-slot>

The final DOM tree contains both a Shadow Root with a p child and the developer's pre element. The latter is linked to the slot node and replaces it's "default" content. The name attribute on the slot tag defines which exact placeholder to use. It is possible to use an unnamed slot as well if there are no available alternatives.

The Shadow DOM specification allows a developer to declare isolated styles and provides an API to use the main document context. Elements inside the Shadow Tree are not accessible for the outer document CSS selectors.

In the following example two styles’ sections are present — one is inside the main document, the other is inside the element’s attached Shadow DOM.

<style>h3 { color: blue; }</style>
<script>
class CustomElement extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>h3 { color: red; }</style>
<h3>Shadow DOM</h3>
`
}
}
customElements.define('my-element', CustomElement);
</script>
<h3>Normal DOM</h3>
<my-element></my-element>

The style applied for a main document’s h3 element has a blue color, meanwhile h3 inside my-element is painted red. It's notable that the main document can define styles for a host element itself. When the my-element style is set to have the green color, it will be applied to it's children, as in a normal style inheritance situation. The inherited color still has less priority than any other defined style.

The :host pseudoclass selector is looking into the Shadow Root's host element.

:host { color: green; }

The styles in the main document have a major priority in comparison with :host styles inside the element. It is possible to use the :host() function for conditional selectors

:host(.error) { color: red; }

When the host has the error class attached, the red color will be applied to it.

Another functional selector is :host-context() that can detect if there is an ancestor matching a specified selector outside of the Shadow Tree.

Similarily, the ::slotted() function can be applied for "external" elements inside slots.

There is still a possibility to customize styles in the main document. That is the CSS custom properties or variables feature applied to the Shadow DOM. It is based on a few principles:

  • The properties are defined and used inside the Shadow DOM. So the component's author is responsible for providing an API (or a list of properties that can be overwritten from outside).
  • Variables are applied to the host element in the main document. As the specification says, custom properties do inherit. That means that if one is not defined in the child element's level, it takes the parent's value.

When the custom element is using Shadow DOM with such a style defined as:

h3 { color: var(--my-color); }

It will try to access the --my-color variable. So, a developer that is using the element can attach a variable in his or her style definitions.

custom-element { --my-color: red; }

However, the --my-color variable is not defined in the h3 selector in any style definition, it is inherited from the Shadow Root's host and it is applied to an internal element's DOM. The h3 color's value is red.

It is reasonable to use the fallback or the default value for those publically-defined variables. If a customer is not specifying the variable, it still has a sensible value.

To summarize, the features that the Shadow DOM offers are:

  • The isolated DOM, separated from the document.
  • Composition of an element either with private or public access to a tree.
  • The slots for the purpose of reusage.
  • The CSS is scoped, so a page's styles are not mixed with ones declared inside the Shadow Tree. This approach simplifies the styles’ definition and decreases the need for hierarchical selectors.

HTML Template

The HTML Template specification is usually combined with Shadow DOM and Custom Elements. It explains a way to specify the layout without influencing the document itself. Any content inside the template tag is not rendered on the page load and can be used later by the JavaScript code to instantiate elements.

<template id="mytemplate">
<script>
alert()
</script>
</template>

In the given example, the browser will now show the popup; however, it is stated inside the script tag.

const template = document.querySelector('#mytemplate')
const clone = document.importNode(template.content, true)
document.body.appendChild(clone)

The popup appears only when the template is instantiated and added to the document. The content is another Document Fragment, which contains the specified HTML structure. The importNode() document's method generates a copy of the given node. The last argument it takes is the "deep" flag. By default it is false and the only high level element will be copied, so true should be used in order to copy the whole layout.

It makes sense to create template elements containing the style definition inside.

<template id="mytemplate">
<style>
:host { color: red }
</style>
</template>

The definition conforms to the natural HTML syntax, is not rendered on the page load and can be used later when a developer needs to append the template's instance.

const template = document.querySelector('#mytemplate')
const div = document.createElement('div')
const root = div.attachShadow({ mode: 'open' })
const clone = document.importNode(template.content, true)
root.appendChild(clone)
document.body.appendChild(div)

Summary

The Custom Elements, Shadow DOM, and HTML Template specifications allow a developer to create reusable components with an encapsulated DOM, styles, and behavior. The majority of the declared APIs are already available in major browsers and have polyfills to mimic the rest.

Have a nice coding and hope to see you in comments!

--

--

Software engineer, instructor, mentor, and author of technical materials #JavaScript