The Quirks of Shadow DOM and How to Take Advantage of Them
Tags: css, webcomponents, shadowdom, webdev
Published: 2020-02-18 04:31:26

> 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](https://dev-to-uploads.s3.amazonaws.com/i/xuc3xnvlh76ur4024mxv.png)

> Source: ["Using Shadow DOM"](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM) by [Mozilla Contributors](https://wiki.developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM$history)

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:

```html
<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>
```

And this is the Light DOM

```html
<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>
```

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

# Answer

{% glitch shadow-dom-quiz file=question-one.html app %}

# 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:

```html
<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>
```

And here's the Light DOM:

```html
<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>
```

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

# Answer

{% glitch shadow-dom-quiz file=question-two.html app %}

# 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:

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

<div>
  Some text
</div>
```

And here's the Light DOM:

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

<question-three></question-three>
<question-three class="custom-color"></question-three>
```

What will happen with the color on this one?

# Answer

{% glitch shadow-dom-quiz file=question-three.html app %}

# 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](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) should help as a starting point.

# 4 - What happens to the children nodes?

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

```html
<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>
```

And here's the Light DOM:

```html
<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>
```

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

# Answer

{% glitch shadow-dom-quiz file=question-four.html app %}

# 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:

```html
<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>
```

And here's the Light DOM:

```html
<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>
```

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

# Answer 5

{% glitch shadow-dom-quiz file=question-five.html app %}

# 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](https://developer.mozilla.org/en-US/docs/Web/CSS/::part).

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