This is part four of a five-part series discussing the Web Components specifications. In part one, we took a 10,000-foot view of the specifications and what they do. In part two, we set out to build a custom modal dialog and created the HTML template for what would evolve into our very own custom HTML element in part three.
Article Series:
An Introduction to Web Components
Crafting Reusable HTML Templates
Creating a Custom Element from Scratch
Encapsulating Style and Structure with Shadow DOM (This post)
Advanced Tooling for Web Components
If you haven’t read those articles, you would be advised to do so now before proceeding in this article as this will continue to build upon the work we’ve done there.
When we last looked at our dialog component, it had a specific shape, structure and behaviors, however it relied heavily on the outside DOM and required that the consumers of our element would need to understand it’s general shape and structure, not to mention authoring all of their own styles (which would eventually modify the document’s global styles). And because our dialog relied on the contents of a template element with an id of “one-dialog”, each document could only have one instance of our modal.
The current limitations of our dialog component aren’t necessarily bad. Consumers who have an intimate knowledge of the dialog’s inner workings can easily consume and use the dialog by creating their own element and defining the content and styles they wish to use (even relying on global styles defined elsewhere). However, we might want to provide more specific design and structural constraints on our element to accommodate best practices, so in this article, we will be incorporating the shadow DOM to our element.
What is the shadow DOM?
In our introduction article, we said that the shadow DOM was “capable of isolating CSS and JavaScript, almost like an
Hello from a closed shadow root!
`;
}
}
We could also save a reference to the shadow root on our element itself, using a Symbol or other key to try to make the shadow root private.
In general, the closed mode for shadow roots exists for native elements that use the shadow DOM in their implementation (like
<#shadow-root>
Hello
#shadow-root>
A given shadow root can have any number of slot elements, which can be distinguished with a name attribute. The first slot inside of the shadow root without a name, will be the default slot and all content not otherwise assigned will flow inside that node. Our dialog really needs two slots: a heading and some content (which we’ll make default).
See the Pen
Dialog example using shadow root and slots by Caleb Williams (@calebdwilliams)
on CodePen.
Go ahead and change the HTML portion of our dialog and see the result. Any content inside of the light DOM is inserted into the slot to which it is assigned. Slotted content remains inside the light DOM although it is rendered as if it were inside the shadow DOM. This means that these elements are still fully style-able by a consumer who might want to control the look and feel of their content.
A shadow root’s author can style content inside the light DOM to a limited extent using the CSS ::slotted() pseudo-selector; however, the DOM tree inside slotted is collapsed, so only simple selectors will work. In other words, we wouldn’t be able to style a element inside a
element within the flattened DOM tree in our previous example.
The best of both worlds
Our dialog is in a good state now: it has encapsulated, semantic markup, styles and behavior; however, some consumers of our dialog might still want to define their own template. Fortunately, by combining two techniques we’ve already learned, we can allow authors to optionally define an external template.
To do this, we will allow each instance of our component to reference an optional template ID. To start, we need to define a getter and setter for our component’s template.
get template() {
return this.getAttribute(template);
}
set template(template) {
if (template) {
this.setAttribute(template, template);
} else {
this.removeAttribute(template);
}
this.render();
}
Here we’re doing much the same thing that we did with our open property by tying it directly to its corresponding attribute. But at the bottom, we’re introducing a new method to our component: render. We are going to use our render method to insert our shadow DOM’s content and remove that behavior from the connectedCallback; instead, we will call render when our element is connected:
connectedCallback() {
this.render();
}
render() {
const { shadowRoot, template } = this;
const templateNode = document.getElementById(template);
shadowRoot.innerHTML = ;
if (templateNode) {
const content = document.importNode(templateNode.content, true);
shadowRoot.appendChild(content);
} else {
shadowRoot.innerHTML = ``;
}
shadowRoot.querySelector(button).addEventListener(click, this.close);
shadowRoot.querySelector(.overlay).addEventListener(click, this.close);
this.open = this.open;
}
Our dialog now has some really basic default stylings, but also gives consumers the ability to define a new template for each instance. If we wanted, we could even use attributeChangedCallback to make this component update based on the template it’s currently pointing to:
static get observedAttributes() { return [open, template]; }
attributeChangedCallback(attrName, oldValue, newValue) {
if (newValue !== oldValue) {
switch (attrName) {
/** Boolean attributes */
case open:
this[attrName] = this.hasAttribute(attrName);
break;
/** Value attributes */
case template:
this[attrName] = newValue;
break;
}
}
}
See the Pen
Dialog example using shadow root, slots and template by Caleb Williams (@calebdwilliams)
on CodePen.
In the demo above, changing the template attribute on our
Strategies for styling the shadow DOM
Currently, the only reliable way to style a shadow DOM node is by adding a
Web components are AWESOME
`;
}
}
customElements.define(other-component, SomeOtherComponent);
In our global CSS, we could target any element that has a part called description by invoking the CSS ::part() selector.
other-component::part(description) {
color: tomato;
}
In the above example, the primary message of the