Shadow DOM Styling

Shadow DOM encompasses both <link rel="stylesheet" href="…"> and <style>. For the first case, the style sheets are HTTP-cached. Hence, they can’t be re-downloaded for multiple components that apply similar templates.

The primary rule is that local styles can operate only within the shadow tree. But, the document styles work out of that tree. Of course, there can be exceptions to the rule.

:host

Let’s explore the :host selector. With it, you can select the shadow host. For instance, let’s try to create <custom-dialog> element that needs to be centered. To meet that goal, we should style the <custom-dialog> element.

Here is how the :host selector operates:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Title of the Document</title>
    <head>
  <body>
    <template id="tmpId">
      <style>
        /* the style will be applied internally to the custom-dialog element */
        :host {
        position: fixed;
        transform: translate(-50%, -50%);
        left: 50%;
        top: 50%;
        display: inline-block;
        border: 2px solid blue;
        padding: 15px;
        }
      </style>
      <slot></slot>
    </template>
    <custom-dialog>  Welcome to W3Docs </custom-dialog>
    <script>
      customElements.define('custom-dialog', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode: 'open'}).append(tmpId.content.cloneNode(true));
        }
      });
    </script>
  </body>
</html>

Cascading

The shadow host (<custom-dialog>) exists in the light DOM. Hence, it obeys the document CSS rules.

In case the property is styled both in the :host and in the document, then the document will take the precedence.

So, if in the document there is:

<style>
  custom-dialog {
  padding: 0;
  }
</style>

Then, <custom-dialog> will be without any padding.

That is very handy, as it allows setting up the default component style in the :host rule. Then, you can override them within the document. There is an exception: once the property is labeled as !important, then the local style will take the precedence for such properties.

:host(selector)

It is similar to the :host but is used in case the host matches the selector.

Here is an example:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Title of the Document</title>
    <head>
  <body>
    <template id="tmpId">
      <style>
        :host([center]) {
        position: fixed;     
        transform: translate(-50%, -50%);
        left: 50%;
        top: 50%;
        border-color: blue;
        }
        :host {
        display: inline-block;
        border: 2px solid red;
        padding: 15px;
        }
      </style>
      <slot></slot>
    </template>
    <custom-dialog center>
      Centre! Welcome to W3Docs.
    </custom-dialog>
    <custom-dialog>
      Not centre. Welcome site.
    </custom-dialog>
    <script>
      customElements.define('custom-dialog', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode: 'open'}).append(tmpId.content.cloneNode(true));
        }
      });
    </script>
  </body>
</html>

As a result, the extra centering styles are used only for the first dialog <custom-dialog centered>.

:host-context(selector)

It is also the same as :host but used only when the shadow host or its ancestors in the outer document correspond to the selector.

Here is an example:

<body class="dark-theme">
  <!-- :host-context(.dark-theme) applies to custom-dialogs inside .dark-theme -->
  <custom-dialog>...</custom-dialog>
</body>

To be more precise, the :host-family of selectors can be used for styling the main element of the component, resting on the context.

Those styles (except for the !important) might be overridden by the document.

Styling Slotted Content

In this section, we are going to consider cases with the slots.

The slotted elements originate from the light DOM. It means that they use document styles. The slotted content is not affected by local styles.

Let’s see an example where the slotted <span> is bold as part of the document style, yet it doesn’t take the background from the local style.

<!DOCTYPE HTML>
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body>
    <style>
      span { font-weight: bold }
    </style>
    <site-card>
      <div slot="sitename"><span>W3Docs</span></div>
    </site-card>
    <script>
      customElements.define('site-card', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode: 'open'});
          this.shadowRoot.innerHTML = `
            <style>
            span { background: blue; }
            </style>
            Sitename: <slot name="sitename"></slot>
          `;
        }
      });
    </script>
  </body>
</html>

So, the result will be bold but not red.

In case you want to style the slotted elements in the components, you can choose among two options.

According to the first one, you can style the <slot>, relying on the CSS inheritance, like this:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body>
    <site-card>
      <div slot="sitename"><span>W3Docs</span></div>
    </site-card>
    <script>
      customElements.define('site-card', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode: 'open'});
          this.shadowRoot.innerHTML = `
            <style>
            slot[name="sitename"] { font-weight: bold; }
            </style>
            Sitename: <slot name="sitename"></slot>
          `;
        }
      });
    </script>
  </body>
</html>

The second option is using the ::slotted(selector) pseudo-class. It corresponds to elements based on the following two conditions:

  1. It’s a slotting element, coming from the light DOM. No matter what the slot name is. But it relates only to the element itself, not the children.
  2. The element corresponds to the selector.

So, in the example, we demonstrate, the ::slotted(div) selects exactly <div slot="username">, not the children of it:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body>
    <site-card>
      <div slot="sitename">
        <div>W3Docs</div>
      </div>
    </site-card>
    <script>
      customElements.define('site-card', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode: 'open'});
          this.shadowRoot.innerHTML = `
            <style>
            ::slotted(div) { border: 2px solid blue; }
            </style>
            Sitename: <slot name="sitename"></slot>
          `;
        }
      });
    </script>
  </body>
</html>

Let’s consider another important thing: the ::slotted selector will not be descended any further in the slot. The selectors like this are invalid:

::slotted(div span) {
  /* our <div> slot does not match this */
}
 
::slotted(div) p {
  /* cannot enter the light of the DOM */
}

Moreover, ::slotted may be used only in CSS. It can’t be used in querySelector.

CSS Hooks with Custom Properties

No selector can be directly affected by shadow DOM styles from the document. But like exposing methods to interact with the component, CSS variables can be exposed for styling it.

Custom CSS properties are both in the light DOM and in the shadow DOM. For instance, in shadow DOM, the --user-card-field-color CSS variable can be used for styling fields. The outer value can set its value like this:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body>
    <style>
      .field {
      color: var(--user-card-field-color, black);
      /* if - user-card-field-color not defined, use black */
      }
    </style>
    <div class="field">
      Name: 
      <slot name="username"></slot>
    </div>
    <div class="field">
      Id: 
      <slot name="id"></slot>
    </div>
    </style>
  </body>
</html>

That property can be declared in the outer document for <user-card> like this:

user-card {
  --user-card-field-color: blue;
}

Custom CSS properties are visible anywhere. So, the inner .field rule can make use of that.

Let’s take a look at the full example:

<!DOCTYPE HTML>
<html>
  <head>
    <title>Title of the Document</title>
  </head>
  <body>
    <style>
      user-card {
      --user-card-field-color: blue;
      }
    </style>
    <template id="tmpId">
      <style>
        .field {
        color: var(--user-card-field-color, black);
        }
      </style>
      <div class="field">
        Name: 
        <slot name="username"></slot>
      </div>
      <div class="field">
        Id: 
        <slot name="id"></slot>
      </div>
    </template>
    <script>
      customElements.define('user-card', class extends HTMLElement {
        connectedCallback() {
          this.attachShadow({mode: 'open'});    this.shadowRoot.append(document.getElementById('tmpId').content.cloneNode(true));
        }
      });
    </script>
    <user-card>
      <span slot="username">Jack Brown</span>
      <span slot="id">112</span>
    </user-card>
  </body>
</html>

Summary

Shadow DOM includes styles such as <link rel="stylesheet"> or <style>. The local styles can impact on:

  • shadow tree
  • shadow host along with the pseudo-classes of :host-family.
  • slotted elements but not their children.

The document styles can impact on:

  • shadow host
  • slotted elements along with their contents

Once there is a conflict between the CSS properties, as a rule, document styles take precedence. The exception is when the property is labeled as !important. In such a case, local styles take precedence.




Do you find this helpful?

Related articles