Web components: styles piercing shadow DOM
Tags: webcomponents, shadowdom, cssvariables, theming
Published: 2022-11-30 00:31:30

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.

```css
/* <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 */
```

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:

```css
/* global-styles.css */
:root {
  --brand-color: blue;
}
```

```html
<!-- our web component template-->
<template>
  <style>
    button {
      background-color: var(--brand-color);
    }
  </style>
  ...
</template>
```

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

```css
/* 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);
}
```

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

```js
// 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);
```

{% embed https://stackblitz.com/edit/web-platform-avj7gx?embed=1&file=my-button.js&view=preview %}

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:
```js
const variant = this.getAttribute('variant') || '';
this.shadowRoot.querySelector('button').className = variant;
```

Was replaced by this CSS:
```css
:host([variant='primary']) button { ... }
:host([variant='secondary']) button { ... }
```

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
<html data-theme="dark">...</html>
```

The magic happens by pure CSS matching :)

# Coming up next
The following article will show how web components **compose** by building a **dropdown**.