DEV Community

Alan Dávalos
Alan Dávalos

Posted on • Updated on

The Quirks of Shadow DOM and How to Take Advantage of Them

Disclaimer:

During this article I'll be referring to the "normal" DOM as "Light DOM" to make it clear when I'm talking about it.

What is Shadow DOM?

While Shadow DOM is considered one of the core standards that make up Web Components it's
actually been in the browsers for quite a while as native HTML Elements like <video>
use it, it just wasn't available for developers to use before it was incorporated in the Web Components standard.

And now that Edge is based on Chromium all Web Component standards, including Shadow DOM, are available in all modern browsers!! 🎉

The whole idea of Shadow DOM is that it allows us to create DOM trees that are isolated
from the Light DOM but that can be appended to the Light DOM and be rendered together with it.

I know that sounds slightly confusing so this image might help understand it better:

Shadow DOM Visualization

Source: "Using Shadow DOM" by Mozilla Contributors

This isolation is the key feature of Shadow DOM and it solves a couple of problems no other approach can really solve:

  1. Nodes inside Shadow DOM aren't accessible via things like document.querySelector() so you can prevent unwanted changes from other scripts.
  2. All CSS in Shadow DOM is scoped to it which has a couple of great benefits:
    1. Styles in Shadow DOM don't leak out to Light DOM
    2. Styles in Light DOM don't bleed in to Shadow DOM*
    3. These two allow you to use super simple selectors like button or #someid without being afraid of anything, this is the biggest reason why we say that Shadow DOM fixes CSS, everything has it's own scope.

Now, you might have noticed that I put an asterisk when I mentioned that Light DOM styles don't bleed in to Shadow DOM.

You see, having Shadow DOM have no way of being stylized from Light DOM would make it a bit too restrictive for many common use cases like theming and light customization.

Which is why Shadow DOM contents can be stylized from Light DOM in certain cases.

Like many CSS-related things, these are slightly difficult to understand just by reading them, so I'm going to take a Quiz-like approach for the rest of this article.

1 - How does inheritance work here?

This is the markup for our first Shadow DOM:

<style>
  .some-class {
    font-family: Arial;
    font-size: 14px;
    color: blue;
  }
</style>

<div>
  This is some text
</div>
<div class="some-class">
  And here's some more text
</div>
Enter fullscreen mode Exit fullscreen mode

And this is the Light DOM

<style>
  .container {
    font-family: Helvetica;
    font-size: 16px;
    color: green;
    padding: 2em;
  }

  .container .some-class {
    color: red;
  }
</style>
<div class="container">
  <!--
  The Shadow DOM from the previous snippet is
  in this web component
  -->
  <question-one></question-one>
</div>
Enter fullscreen mode Exit fullscreen mode

How do you think this will render? Try not to spoil yourself 😉.

Answer

Why?

Nodes inside Shadow DOM by default have no special styles, so properties that inherit such as
font-family or color will inherit their values from the Shadow DOM's ancestors.

Now, as you can see, in some-class we're actually defining font-family, font-size, and color so that part doesn't inherit styles.

Bonus points if you noticed that the .container .some-class selector in the Light DOM is basically useless as it's trying to affect nodes inside the Shadow DOM directly which isn't allowed.

2 - Can you style the host?

Here's the Shadow DOM:

<style>
  /*
  This selector basically applies to the root
  of our shadow DOM
  */
  :host {
    display: block;
    width: 30px;
    border: 1px dotted black;
  }
</style>

<div>1</div>
<div>2</div>
<div>3</div>
Enter fullscreen mode Exit fullscreen mode

And here's the Light DOM:

<style>
  .some-class {
    display: flex;
    flex-flow: row-reverse;
    width: 100%;
    border: 1px solid red;
  }
</style>
<question-two></question-two>
<question-two class="some-class"></question-two>
Enter fullscreen mode Exit fullscreen mode

Once again, how do you think this will render? Ready?

Answer

Why?

Unlike styles applied to the children nodes, host-level styles in Shadow DOM can be overridden from Light DOM.

Basically, anything that you define for the host directly is no more than a default style so if you really want to enforce a certain style you probably want to add a container node as a children and style that instead.

3 - Are CSS variables applied?

Once more, here's the Shadow DOM:

<style>
  div {
    color: var(--my-color, blue);
  }
</style>

<div>
  Some text
</div>
Enter fullscreen mode Exit fullscreen mode

And here's the Light DOM:

<style>
  .custom-color {
    --my-color: red;
  }
</style>

<question-three></question-three>
<question-three class="custom-color"></question-three>
Enter fullscreen mode Exit fullscreen mode

What will happen with the color on this one?

Answer

Why?

CSS Custom Properties (or CSS Variables as most people call them) are one of the few things that can freely style nodes inside a Shadow DOM.

Of course, like on this example, the style used in the Shadow DOM must define which variables it wants to use and where, but this is an easy way of providing customization on key points.

For more information on CSS Variables this article should help as a starting point.

4 - What happens to the children nodes?

Ready for one more question? Here's the Shadow DOM:

<style>
  /*
  the ::slotted(selector) selector applies to
  any nodes appended in the indicated slot
  that match the given selector
  */
  header ::slotted(*) {
    text-decoration: underline;
  }
  article ::slotted(div.special) {
    color: turquoise;
  }
</style>

<section>
  <header>
    <h3><slot name="header">Default Header</slot></h3>
  </header>
  <article>
    <slot></slot>
  </article>
</section>
Enter fullscreen mode Exit fullscreen mode

And here's the Light DOM:

<style>
  .some-class .header {
    color: red;
  }

  .some-class div {
    color: mediumvioletred;
  }
</style>

<question-four>
  <div>1</div>
  <div class="special">2</div>
  <div>3</div>
</question-four>

<question-four class="some-class">
  <div>4</div>
  <div class="special">5</div>
  <div>6</div>
  <span class="header" slot="header">
    Second Element Header
  </span>
</question-four>
Enter fullscreen mode Exit fullscreen mode

This one had a bit more things to figure out so take your time, how do you think it will render?

Answer

Why?

If this is your first time reading about Shadow DOM I won't blame you if you didn't get this one, but bear with me, I'll do my best to explain this.

You see, in Shadow DOM there's a special <slot> tag that serves to basically say what part of the template this element's children will be appended to.

If you've used children in React or slots in Vue you probably are familiar with the concept (Vue's slots are based on this standard).

Now, in styles defined in Shadow DOM we can try to apply styles to children appended through a slot using the ::slotted() selector.

But as you can see, this works really similar to how host level styles behaved a couple questions ago. Styles applied with ::slotted() are also basically default styles that will merge with the styles coming from Light DOM but the Light DOM styles will have a higher priority.

5 - Can you style some parts only?

The last question!! Are you ready? Here's the Shadow DOM:

<style>
  :host {
    display: flex;
  }
  img {
    width: 64px;
    height: 64px;
  }
  div {
    font-family: Arial;
    color: seagreen;
  }
</style>

<img part="avatar" src="https://via.placeholder.com/64x64.jpg?text=Avatar" />
<div part="name">
  Some Name
</div>
Enter fullscreen mode Exit fullscreen mode

And here's the Light DOM:

<style>
  question-five.custom-part::part(avatar) {
    border-radius: 50%;
  }

  question-five.custom-part::part(name) {
    font-family: Verdana;
    color: mediumpurple;
  }
</style>

<question-five></question-five>
<question-five class="custom-part"></question-five>
Enter fullscreen mode Exit fullscreen mode

For the last time, how will this part stuff render?

Answer 5

Why?

Now we're entering into cutting edge grounds, unlike the content for all the four previous questions which are all available in all modern browsers, this last one is not yet available in Safari (it's probably coming soon-ish as it's on the Technology Preview and in my tests it worked just fine), but it's available on all Chromium-based browsers (Chrome, Edge, Opera, etc.) and Firefox.

Update: As of October 2020, Safari has implemented this standard so now it works on all modern browsers!

For this demo we use the Shadow Parts Standard, and as the name suggests, it let's us declare "parts" inside our Shadow DOM that can then be styled from Light DOM using the ::part() pseudo element.

As you can probably tell by now, Shadow Parts work similarly to how slots and host level styles work so the properties set using ::part() on the Light DOM styles will take top priority and styles set on the Shadow DOM will work as fallbacks.

To Close

Shadow DOM is a really powerful part of the Web Standard but it definitely has some quirks that aren't that evident at first glance.

I hope this little quiz helped you understand a bit better how Shadow DOM works and how you can make the most of it both when using and when creating your own Web Components (or even if you just use Shadow DOM by itself).

More Info

https://developers.google.com/web/fundamentals/web-components/shadowdom
https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM

Discussion (1)

Collapse
brianboyko profile image
Brian Boyko

This is all very, very good information. I'm just disappointed in that it does illustrate why you would also NOT want to use web components in a lot of cases. Specifically, when you said:

this is the biggest reason why we say that Shadow DOM fixes CSS, everything has it's own scope.

Honestly, that's not what I expect CSS behaviour to be. To me, not being able to alter the styling of items in the shadow dom from the light dom breaks CSS - specifically, the rules about CSS specificity and cascading that we've been working with for the past 20 years.

It does make my particular use case problematic.

Right now my team lead is REALLY pushing for web components to be used as the base of a UI library; but the problem is that if our end-users can't alter the CSS inside of a web-component, then if the CSS doesn't match 100% what they need it to be, they have to write an entirely new custom component from scratch instead of using the UI library (defeating the purpose of having a reusable UI library.) Or, alternatively, they could ask for changes to be made in the UI library itself... if they're willing to wait in the queue for their issue to be resolved.

All of this would be acceptable if there was a way to turn off CSS Encapsulation without turning off functionality, but there isn't.

This is a classic case of where code written to prevent stupid people from doing stupid things also prevents clever people from doing clever things.