Slots Shadow Dom
Luckily, we don’t have to. Shadow DOM supports slot elements, that are automatically filled by the content from light DOM. Let’s see how slots work on a simple example. Here, shadow DOM provides two slots, filled from light DOM. Right now the logic goes like: if you're slotted, traverse your slots and collect rules in their shadow trees as needed. This is the code This is nice because the complexity of styling the element depends directly on the complexity of the shadow trees that you're building, and it only affects slotted nodes.
When talking about Web Components we often forget that it´s an umbrella term that covers a set of low level API’s that work together to form the web´s native component model.
It is a very common misconception that we need to use them all in order to build Web Components.
In fact, we really only need the custom element API in order to register our component name and class with the browser. However, combining custom elements with shadow DOM gives us out-of-the-box style isolation and DOM encapsulation, which is perfect for self-contained reusable components for our UIs.
Creating a Web Component that does not use shadow DOM is perfectly fine, and in some cases, i´d advise against using shadow DOM at all.
Let us go over some use-cases where I think shadow DOM might not be the right choice. But before that, a quick overview of what shadow DOM provides.
The short intro to shadow DOM
Shadow DOM is all about encapsulation. Due to the global nature of HTML, CSS and Javascript we´ve developed a lot of tools and methodologies to circumvent the issues over the years.
Common issues include clashing element Id´s, classes or styles from the global stylesheet overriding 3rd party libraries and/or vice versa. Some of us still have to keep these things in mind when developing today depending on tooling.
Shadow DOM fixes this by giving us:
- Isolated DOM tree: The shadow DOM is self-contained and the outside cannot query elements on the inside (e.g.
document.querySelector
wont return nodes from within the shadow tree) - Scoped CSS: Styles defined within the shadow DOM will not leak out, and outside styles will not bleed in.
- Composition: Through the use of
<slot />
our elements can take outside nodes from the light DOM and place them in specific positions inside the shadow DOM.
The scoped CSS alone is incredibly powerful. Frameworks today all include some form of scoped styling that, during compile time, adds an attribute to the DOM element that is also added to the output CSS. This combination results in a very specific selector in your css (a[data-v-fxfx-79]
) that won´t bleed out and affect the outside DOM.
However, this method does not prevent outside styles from leaking into your component. This is where the true power of shadow DOM scoped styling really shines. Not only is it native to the browser, but it works both ways.
So why not always use shadow DOM? 🤔
We´ve just learned that the shadow DOM API gives us a set of incredibly powerful tools that enable us to build truly encapsulated reusable components. So why not use it everywhere?
First of all, without a clear goal or use-case in our mind, we probably shouldn’t just jump the gun and start enabling shadow DOM everywhere. As with every new technology, we should first do our research.
Browser support
Whenever we look at cool new browser API´s we have to also take support into consideration. Luckily, shadow DOM is supported in all major browsers. However, some of us has to still support older browser like IE11 for a while still.
We could polyfill for our IE11 users, right? 🤷♂️
While polyfilling shadow DOM is possible, it is pretty hard, and the existing polyfills are invasive and slow.
So instead of directly polyfilling the shadow DOM, compilers such as stencilJS fall back to scoped styles for IE11. While this does make our component usable, it also reintroduces the issue of scoped styling not preventing outside styles from bleeding in.
This means that we have to cautiously test in IE11 that outside styles won´t affect the insides of our component. That sucks, as our component now behaves differently between browsers.
So even though your components might be great candidates for shadow DOM, carefully weigh your options if you´re forced to support IE11.
Who are our consumers?
The next thing I suggest looking into is, who are we making these components for? Is it our own internal product or are we making a component library to be consumed by the masses on npm ?
A friend of mine said it quite well; A single component that needs to stand on its own with its own set of functionality is a good candidate for shadow DOM. While one or more components as part of an application might not need shadow DOM, as their intended use is much clearer and their markup less fragile.
The quote above got me thinking about the whole internal vs external thing. When introducing web components to an existing long running project, there is a good chance we already have some sort of design system in place already. Or at the very least, an extensive set of battle tested styles and markup.
With this in mind, we should really think about what shadow DOM could solve for us that we haven´t already solved by using methodologies such as BEM or ITCSS,or just a solid CSS structure.
Say we have the following classes in our design system stylesheet:
Now let us add a new reusable component to the project:
💡 I´m using stencil, a web component compiler, in my example above
At first glance we might expect our new <fancy-card>
component to justwork. We´ve added the classes from our stylesheet, they worked before we added the component, so all is good, right?
Not exactly…
When we see the element in the browser, the only style applied will be from the .card
class on the <fancy-card>
element. This is because the element has a shadow root attached to the host element (<fancy-card>
), and as such, the divs within the component cannot be styled via CSS classes defined outside the component shadow root.
Remember how global styles won´t bleed into the shadow DOM? Well, this is how it works.
We have no way of using our existing classes unless we refactor and include those styles inside the component shadow root. If the existing design system relies on sass variables, we´d also need to import those in the component stylesheet.
While refactoring itself is not a problem, as we do it all the time, the reason for which we are refactoring is. By moving the above HTML and CSS into the component, we haven´t solved anything that wasn´t already solved before.
Now, I´m aware that the <fancy-card>
component might seem like a dumb example at first glance, but I´ve actually seen a lot of these components out there. In fact, I´ve done it myself when I first started looking into Web Components and thought I needed to convert everything.
The solution to the above could instead be to turn off shadow DOM. The issue of the class styles not being applied inside the component would go away and we would still have a composable component ready to use.
Stencil enables using <slot>
without shadow DOM enabled. In a vanilla web component the above markup would not work as natively, <slot>
is part of the shadow DOM API.
Some would probably argue that with the rather simple markup for the component and no complex functionality, it should not require javascript at all. Since it is merely a glorified div element. While I do agree that such a simple component should not require javascript, if it was to be part of a consumable component library, using it would be a lot easier than having to add the html structure plus the classes as a consumer. As long as we´re aware of the trade-offs!
A note on forms
In a previous article, Custom elements, shadow DOM and implicit form submission, I mentioned that we cannot query the shadow tree from the outside, elements such as input
or textarea
placed inside our shadow root will not work with an outside <form>
element. The inputs would simply be ignored as they are not in the same tree-order as the form.
So if we wanted to create a custom input component. We would have to either write custom functionality to circumvent this issue or…
🥁🥁🥁
Just not use shadow DOM 🤷♂️
Conclusion
Ultimately, shadow DOM is not a requirement in order to build Web Components. However, the great synergy between shadow DOM, custom elements and CSS variables is worth exploring. There are already tons of great projects and stand-alone components out there that show the power and versatility of these API´s combined.
I hope my post helped clear some of the confusion around shadow DOM and how it can help us tremendously when building Web Components.
Web Components provide a component model to the Web. Web Components, instead of being a single spec, is a collection of several stand-alone Web technologies. Often Web Components will leverage Shadow DOM features. Shadow DOM is commonly used for CSS encapsulation. However, Shadow DOM has another useful feature called Slots.
The Slot API is a content projection API that allows HTML content from the host application to be rendered into your component template. Common examples of this are things like cards and modals.
Here is a minimal example of a Custom Element using the Slot API.
The tags’ content can be rendered into our template we defined. The browser render the content wherever the <slot>
element is placed. If we look at what the browser renders, we will see something like this:
The content is projected and rendered within the template of our component. Often there are use cases, whereas the component author we would like to know about any updates to the content provided by the slot element. We can achieve this by adding an event listener in our component for the slotchange
event.
Slots Shadow Domain
This event will fire whenever any content has changed within the slot. To test this, we can use our component and dynamically update the content to see the event update.
Slots Without Shadow Dom
In this example, every one second, we can set the textContent
or the innerHTML
of the component and see the slotchange
event fire within the x-component
.
We can easily render content into our component templates and listen for content updates. But there is one interesting exception to this rule. While the event will happen whenever textContent
or innerHTML
are set, the event will not occur if a textNode
reference is updated dynamically. Let’s take a look at an example.
Instead of directly setting the textContent
or innerHTML
of our element we create a text node. While not an HTML element, the text node allows us to hold a reference in memory we can update at a later point. So if we go back to our interval, we will see the text change, but the event is no longer triggered.
This behavior can be a bit unexpected at first. Many JavaScript frameworks will leverage text nodes to optimize for performance. The short rule to remember is slotchange
only fires when the HTML DOM has changed either by a DOM/Text Node from being added or removed. Check out the full working example below!