lamplightdev

How to share styles in the Shadow DOM

Tue Mar 23 2021

The Shadow DOM is great for insulating your Web Components from global style rules, but what do you do if you want to share common styling between components? One approach is to duplicate style rules across components but that can be inefficient and a maintenance headache - surely there's another way?


The problem

Let's take a simplified Card component containing a button, and place it on a page also containing a button:

class MyCard extends HTMLElement {
constructor() {
super();

this.attachShadow({ mode: 'open' });
}

connectedCallback() {
// these should be sanitized!
const title = this.getAttribute('my-title');
const content = this.getAttribute('my-content');

this.shadowRoot.innerHTML = `
<style>
:host {
display: flex;
flex-direction: column;
border: 1px solid #ddd;
border-radius: 0.2rem;
}

#title {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
font-size: 2rem;
border-bottom: 1px solid #ddd;
}

#content {
padding: 1rem;
}
</style>

<div id="title">
${title}
<button>I am inside a component, click me!</button>
</div>

<div id="content">
${content}</div>
`
;
}
}

customElements.define('my-card', MyCard);
<p>
<button>I'm not in a component</button>
</p>

<my-card my-title="Hello" my-content="Welcome to the jungle!"></my-card>

which gives us:

Unstyled buttons


The challenge then is how to style the button so that it looks the same both inside and outside of your component. Let's use the following CSS to style your button:

button {
border: 0;
padding: 0.5rem;
border-radius: 0.2rem;
background-color: steelblue;
color: white;
}

Unstyled buttons

Where do you put these styles so they apply to the outer page and inside your component?


The wrong way

The wrong way is to add those styles to your page's stylesheet:

<link rel="stylesheet" href="/button.css" />
<!-- button.css contains the buttons styles above -->

AND in the style block inside your component's Shadow DOM:

<style>
button {
/* button styles here */
}

/* your component specific styles go here*/
</style>

As you may have spotted, this has several limitations:

  1. Duplication - if you want to change your button styling, you have to update it in your stylesheet and in every component that contains a button.
  2. Wasted bytes - the browser has to download the same CSS for the outer page and for every component.
  3. Not dynamic - if you want to update the styling dynamically then you are out of luck.

A better way

Luckily <link> tags are valid inside the Shadow DOM as well as in your outer page, so you can use the link from the outer page:

<link rel="stylesheet" href="/button.css" />
<!-- button.css contains the buttons styles above -->

and re-use it in your component's Shadow DOM:

...
this.shadowRoot.innerHTML = `
<link rel="stylesheet" href="/button.css" />
<style>
/* your component specific styles go here */
</style>
...
`
;
...

In this way you:

  1. Avoid duplication - you only have to write your styles once, inside the stylesheet.
  2. No wasted bytes - as long as the stylesheet is sent with sensible caching headers, only the first time the stylesheet is encountered will it need to be downloaded. Subsequent requests for the stylesheet will come straight from the cache.

Dynamic styles

But one issue remains with this better approach - if you want to dynamically update the styling of your button there still isn't once place where you can change the style and have it update the style of all your buttons.

Both the outer page and each of your components are using a copy of the same stylesheet, not a single instance, so changing a style in one instance of the stylesheet won't be replicated in all the other instances.

Now this may well not be an issue if you don't need this functionality, in which case crack open the champagne and put your dancing shoes on - you're all set. But if you do, you have a 2 further options:

  1. CSS Custom Properties (CSS variables) - CSS custom properties defined on the outer document are available automatically inside your Shadow DOM. So you could define some custom properties in your document, and refer to them in your button's styles. Updating the properties in JavaScript would then apply them to all your button instances. This works but does mean you have to add lots of custom properties if you want to control all aspects of styling, and you still can't add new styles this way.
  2. Constructable Stylesheets - Constructable Stylesheets are a proposal to address the exact issue of reusing the same stylesheet across documents and Shadow roots, and providing a simpler way to add and update styles in a stylesheet. Unfortunately they have only been implemented in Chrome (with only tepid support from other browsers) so they may not be a viable option, although a polyfill is available. Find out more in the Google developer docs.

Wrap-up

Using the same <link> tag both in your outer document and inside your component's Shadow DOM is currently the best way to share styles across your components without code duplication, while CSS custom properties provide a well supported, albeit somewhat limited, way of dynamically updating shared styles. Constructable Stylesheets promise a better approach to re-using and dynamically updating styles but with limited support at this time.


Did you enjoy this post?

I'd love to know - send me a message on twitter @lamplightdev or sign up for my occasional newsletter on Web Components and Progressive Enhancement - your email will never be shared with anyone else.