DEV Community

Cover image for Web components: styles piercing shadow DOM
Joan Llenas Masó
Joan Llenas Masó

Posted on

Web components: styles piercing shadow DOM

The web Components v1 specification consists of three main technologies that can be used to create reusable custom elements:

  • Custom elements
  • HTML templates
  • Shadow DOM

:host

In addition to these technologies, CSS has also been updated to support web components. For instance, the new :host pseudo selector has been added to select the web component from inside itself.
This pseudo selector can also be combined like you would in other scenarios.

/* <my-button> styles block */

:host{} /* selects the my-button tag */
:host(.primary){} /* selects the my-button when it has the primary class */
:host([variant="primary"]){} /* selects the my-button when the `primary` variant is set */
Enter fullscreen mode Exit fullscreen mode

We'll see this in action in the following example.

Theming with CSS variables

There are different ways to pierce the shadow DOM when it comes to styling, the main one being CSS variables.
Quick example:

/* global-styles.css */
:root {
  --brand-color: blue;
}
Enter fullscreen mode Exit fullscreen mode
<!-- our web component template-->
<template>
  <style>
    button {
      background-color: var(--brand-color);
    }
  </style>
  ...
</template>
Enter fullscreen mode Exit fullscreen mode

Here, the template's button background will get the --brand-color defined in :root. In a nutshell, that's the primary mechanism for theming web components.

Theming the <my-button> component

/* styles.css */
:root {
  --background-color: #fff;

  --color-brand: #0b66fa;
  --color-white: #fff;
  --color-black: #000000de;

  --background-low-contrast: var(--color-white);
  --background-high-contrast: var(--color-black);

  --text-low-contrast: var(--color-white);
  --text-high-contrast: var(--color-black);

  --border-low-contrast: 1px solid var(--color-white);
  --border-high-contrast: 1px solid var(--color-black);
}

html[data-theme='dark'] {
  --background-color: #212a2e;

  --background-low-contrast: var(--color-black);
  --background-high-contrast: var(--color-white);

  --text-low-contrast: var(--color-black);
  --text-high-contrast: var(--color-white);

  --border-low-contrast: 1px solid var(--color-black);
  --border-high-contrast: 1px solid var(--color-white);
}

body {
  background-color: var(--background-color);
}
Enter fullscreen mode Exit fullscreen mode
<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <link rel="stylesheet" href="styles.css" />
    <script src="my-button.js"></script>
    <script>
      function toggleTheme() {
        if (document.querySelector('html').dataset.theme === 'dark') {
          document.querySelector('html').dataset.theme = '';
        } else {
          document.querySelector('html').dataset.theme = 'dark';
        }
      }
    </script>
  </head>

  <body>
    <template id="my-button-template">
      <style>
        button {
          cursor: pointer;
          font-size: 20px;
          font-weight: 700;
          padding: 12px;
          min-width: 180px;
          border-radius: 12px;
        }
        :host([variant='primary']) button {
          background-color: var(--color-brand);
          color: var(--color-white);
          border: 0;
        }
        :host([variant='secondary']) button {
          border: var(--border-high-contrast);
          background-color: var(--background-low-contrast);
          color: var(--text-high-contrast);
        }
      </style>
      <button><slot></slot></button>
    </template>

    <my-button variant="primary" onclick="toggleTheme()"
      >Toggle theme</my-button
    >
    <my-button variant="secondary">Secondary</my-button>
    <button class="primary">Plain HTML &lt;button&gt;</button>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode
// my-button.js
class MyWebComponent extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('my-button-template').content;
    const shadowRoot = this.attachShadow({ mode: 'open' });
    shadowRoot.appendChild(template.cloneNode(true));
  }
}

customElements.define('my-button', MyWebComponent);
Enter fullscreen mode Exit fullscreen mode

By clicking the Toggle theme button, you will see how the overall look and feel changes from light to dark and vice versa.

A couple of interesting facts about this example:

  • We've eliminated all style-handling logic from the component and moved it to the CSS declaration. In particular:

This Js code:

const variant = this.getAttribute('variant') || '';
this.shadowRoot.querySelector('button').className = variant;
Enter fullscreen mode Exit fullscreen mode

Was replaced by this CSS:

:host([variant='primary']) button { ... }
:host([variant='secondary']) button { ... }
Enter fullscreen mode Exit fullscreen mode

So we could remove all life cycle callbacks and the render() function.

  • Activating the dark theme is as easy as adding data-theme="dark" to the main <html> tag.
<html data-theme="dark">...</html>
Enter fullscreen mode Exit fullscreen mode

The magic happens by pure CSS matching :)

Coming up next

The following article will show how web components compose by building a dropdown.

Top comments (0)