DEV Community

Poul H. Hansen
Poul H. Hansen

Posted on • Originally published at hjorthhansen.dev

You might not need shadow DOM

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:

    .card {...}
    .card__header {...}
    .card__body {...}
    .card__footer {...}

Now let us add a new reusable component to the project:

@Component({
    tag: 'fancy-card',
    shadow: true
})
export class FancyCardComponent {
    render() {
        return (
            <Host class="card">
                <div class="card__header">
                    <slot name="header"></slot>
                </div>
                <div class="card__body">
                    <slot></slot>
                </div>
                <div class="card__footer">
                    <slot name="footer"></slot>
                </div>
            </Host>
        )
    }
}

💡 I´m using stencil, a web component compiler, in my example above

At first glance we might expect our new <fancy-card> component to just work. 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.

<fancy-card>
    <h2 slot="header">Awesome product</h2>
    <p>lorem ipsum...</p>
    <button slot="footer">Buy</button>
</fancy-card>

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.

Top comments (0)