Using Global Styles in Shadow DOM
One of the most common misunderstandings folks seem to have about Web Components is that they can’t take advantage of global CSS. This is simply not true. With only a few lines of JavaScript you can enable any Web Component to respond to your global CSS. In this article, I’ll show you how to build this into your own components, as well as how to take pre-existing FAST and Lit components and modify them to also respond to global styles. We’ll even see how to accomplish this with Declarative Shadow DOM (DSD).
Shadow DOM, by design, provides encapsulation of styles. This means that only the styles that you explicitly add to your shadow root will affect its presentation. Likewise, only the styles that you explicitly leak out will affect external DOM. One only needs to leverage a couple of standard HTML features to import global styles into a shadow root. Let’s see how this works…
Using Global Styles in a VanillaJS Web Component
Let’s begin by looking at some HTML containing global styles and two web components, each of which contains the same h1
and h2
structure as the body
.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Global Styles in Shadow DOM</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<style>
h2 {
color: red;
}
</style>
</head>
<body>
<h1>Light DOM H1 with Bootstrap Styles</h1>
<h2>Light DOM H2 with Bootstrap and Global Custom Styles</h2>
<hr>
<not-using-global-styles></not-using-global-styles>
<hr>
<using-global-styles></using-global-styles>
<hr>
</body>
</html>
In the above HTML, I have a standard CSS link
to Bootstrap. I also have some custom styles provided via a style
element. Here’s how the page renders.
The h1
and h2
that are directly in the body
pick up both Bootstrap and the custom styles. The h1
and h2
that are in the not-using-global-styles
custom element do not pick up the global CSS. Both of these cases are as expected. But the h1
and h2
that are in the using-global-styles
do pick up both Bootstrap and the custom styles.
How is this accomplished?
If we compare the implementation of not-using-global-styles
to that of using-global-styles
, we’ll see one important difference.
not-using-global-styles.js
export class NotUsingGlobalStyles extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" }).innerHTML = `
<h1>Shadow DOM H1 without Bootstrap Styles</h1>
<h2>Shadow DOM H2 without Bootstrap or Global Custom Styles</h2>
`;
}
}
customElements.define("not-using-global-styles", NotUsingGlobalStyles);
using-global-styles.js
import { addGlobalStylesToShadowRoot } from "./global-styles.js";
export class UsingGlobalStyles extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" }).innerHTML = `
<h1>Shadow DOM H1 with Bootstrap Styles</h1>
<h2>Shadow DOM H2 with Bootstrap and Global Custom Styles</h2>
`;
addGlobalStylesToShadowRoot(this.shadowRoot); // look here!
}
}
customElements.define("using-global-styles", UsingGlobalStyles);
The magic is in a single helper function that I’ve written: addGlobalStylesToShadowRoot
. All you need to do to inherit global CSS in your Shadow DOM is import that function and call it with your shadow root. Here’s how it’s implemented:
global-styles.js
let globalSheets = null;
export function getGlobalStyleSheets() {
if (globalSheets === null) {
globalSheets = Array.from(document.styleSheets)
.map(x => {
const sheet = new CSSStyleSheet();
const css = Array.from(x.cssRules).map(rule => rule.cssText).join(' ');
sheet.replaceSync(css);
return sheet;
});
}
return globalSheets;
}
export function addGlobalStylesToShadowRoot(shadowRoot) {
shadowRoot.adoptedStyleSheets.push(
...getGlobalStyleSheets()
);
}
By leveraging CSSStyleSheet
and the shadow root’s adoptedStyleSheets
array, we can add any style sheet we want to our Shadow DOM, global or otherwise. In the code above, I’ve written a function called getGlobalStyleSheets()
that looks at the document.styleSheets
collection and creates CSSStyleSheet
instances using standard APIs. The function lazily constructs the sheets and then caches them, so that the work is only done when needed and all sheet instances can be shared across all Shadow DOMs that need them.
NOTE: The style sheets we get from
document.styleSheets
are not “Constructible Style Sheet” instances, so we can’t directly use them withadoptedStyleSheets
. No problem. We just create our own constructible style sheets from the css text of the document sheets.
Really, that’s all there is to it. Just a few lines of JavaScript using standard APIs will make global styles available in any Web Component where you need them.
Want to know more about CSSStyleSheet
, adoptedStyleSheets
, Declarative Shadow DOM, and other Shadow DOM and styling topics discussed in this blog? Check out my Web Component Engineering course. Group rates for teams and PPP pricing available upon request.
Adding Global Styles to 3rd Party Web Components
The code above seems fine if you are the author of the Web Component. But what if you need to use global styles with a component created by someone else who used a Web Component Library like FAST or Lit? You could use the addGlobalStylesToShadowRoot()
helper function on each instance’s shadow root (assuming it’s open). That’s not very practical though. Is there a way that you could control this for all instances of a component type?
Yes. Let’s take a look at each library to see how.
Adding Global Styles to an Existing FAST Element
Let’s start by looking at some very basic code for a FASTElement
that doesn’t use global styles. Imagine this was written by another team, or that it was part of an open-source component library you are using.
import { FASTElement, FASTElementDefinition, html } from "@microsoft/fast-element";
export const template = html`
<h1>Existing FAST Element Shadow DOM H1</h1>
<h2>Existing FAST Element Shadow DOM H2</h2>
`;
export class ExistingFASTElement extends FASTElement {
}
export const definition = new FASTElementDefinition(ExistingFASTElement, {
name: "existing-fast-element",
template
});
In FAST, all Web Components have a FASTElementDefinition
that provides metadata about the component, used when constructing instances. 3rd-party component libraries built on FAST usually provide these definitions, decoupled from component registration. As a result, we can easily add global styles to all instances of a Web Component type using a small amount of code that modifies the definition itself. Here’s how we do it:
import { ElementStyles } from "@microsoft/fast-element";
import { definition } from "./existing-fast-element.js";
import { getGlobalStyleSheets } from "./global-styles.js";
// FAST
definition.styles = ElementStyles.create(
getGlobalStyleSheets(),
definition.styles
);
definition.define();
FAST provides an ElementStyles
helper for working with style sheets. We can simply pass our global styles, provided by getGlobalStyleSheets()
, along with any existing styles defined by the component. This will then return a new set of styles associated with the definition that will be used by all instances of the component.
NOTE: Be mindful of the ordering of the style sheets. In the above example, we have ordered the sheets so that the global styles come before the component’s built-in styles. If that’s not desired, you can always swap the order.
Adding Global Styles to an Existing Lit Element
Similarly, we can look at some very basic code for a LitElement
that doesn’t use global styles.
import { LitElement, html } from "lit";
export class ExistingLitElement extends LitElement {
render() {
return html`
<h1>Existing Lit Element Shadow DOM H1</h1>
<h2>Existing Lit Element Shadow DOM H2</h2>
`;
}
}
Lit looks for styles in a static styles
field on the class. So, all we need to do is alter that to add the global styles. Here’s what that looks like:
import { ExistingLitElement } from "./existing-lit-element.js";
import { getGlobalStyleSheets } from "./global-styles.js";
// Lit
ExistingLitElement.styles = [
...getGlobalStyleSheets(),
...(Array.isArray(ExistingLitElement.styles)
? ExistingLitElement.styles
: ExistingLitElement.styles ? [ExistingLitElement.styles] : [])
];
customElements.define("existing-lit-element", ExistingLitElement);
This looks slightly more complicated, mainly because we need to do a bit more work to ensure that we’re handling any existing styles on the 3rd-party element. So, just a bit of conditional logic checking for undefined, single, and array possibilities is needed. But the concept is the same as with FAST. We just get the global styles and merge them with any existing styles, then assign that back to the styles
static field. Done.
Adding Global Styles to Declarative Shadow DOM (DSD)
What if we want to enable global styles to work in a Declarative Shadow DOM (DSD)? How would we accomplish that?
Well, we’re going to use the same helper, but we’re going to wrap it in a very simple custom element that will add the global styles while the DSD is being streamed into the DOM. Here’s the small element that does the work:
adopt-global-styles.js
import { addGlobalStylesToShadowRoot } from "./global-styles.js";
export class AdoptGlobalStyles extends HTMLElement {
connectedCallback() {
addGlobalStylesToShadowRoot(this.getRootNode());
this.remove();
}
}
customElements.define("adopt-global-styles", AdoptGlobalStyles);
And here’s how we use it in DSD:
<dsd-element>
<template shadowrootmode="open">
<adopt-global-styles></adopt-global-styles>
<h1>DSD H1 with Bootstrap Styles</h1>
<h2>DSD H2 with Bootstrap and Global Custom Styles</h2>
</template>
</dsd-element>
The adopt-global-styles
element uses getRootNode()
to obtain the shadow root that it is inside of. In this case, the shadow root created by DSD. It then adds the global styles to that root and removes itself once the work is done.
Standards, Experiments, and Helper Libraries
While it’s possible to use global styles in your Shadow DOM, many would like a built-in standard enabling this, so no JavaScript is required. To that end, various conversations have been happening, attempting to hash out what the key scenarios are and what the API might look like. These conversations are happening under the banner of “open stylable shadow roots”. If you are interested in this, I highly recommend that you give Brian Kardell’s blog post a read and check out his half-light library.
Brian is eagerly seeking feedback on scenarios and APIs to help shape a future HTML/CSS standard. Please give half-light
a try in your own Web Components and help us out in shaping the future of the Web.
Wrapping Up
I hope this article helps dispel the myth that you can’t use global styles with Shadow DOM. A very small amount of JavaScript that interacts with document.styleSheets
and adoptedStyleSheets
will often do the trick. One simple helper function can enable scenarios in VanillaJS, FAST, Lit, and DSD. For more advanced, nuanced, and automatic techniques, a small 100 LOC library like half-light can be used, a steppingstone to a potential new HTML/CSS standard.
If you enjoyed this look into Web Standards, please let me encourage you to consider purchasing my Web Component Engineering course for yourself or your team. I’d also love it if you would subscribe to this blog, subscribe to my YouTube channel, or follow me on twitter. Your support greatly helps me continue writing and bringing this kind of content to the broader community. Thank you!