Share Compiled CSS with adoptedStyleSheets
The adoptedStyleSheets property is a modern Web API
feature that allows developers to apply constructable stylesheets
directly to a Document or Shadow DOM root. When building JavaScript Web
Components, styling encapsulated shadow roots historically required
duplicating <style> tags inside every component
instance, which increased memory consumption and degraded performance.
By using adoptedStyleSheets, you can instantiate and parse
a compiled CSS stylesheet once in memory and share that single reference
across multiple custom element instances, ensuring optimal rendering
performance and straightforward theme management.
Understanding Constructable Stylesheets and adoptedStyleSheets
adoptedStyleSheets is part of the Constructable
Stylesheets specification. It works in tandem with the
CSSStyleSheet() constructor, allowing developers to create,
modify, and share CSS rules programmatically without needing to insert
raw HTML markup or inline <style> tags into the
DOM.
Both the Document and ShadowRoot interfaces
expose the adoptedStyleSheets property as an array of
CSSStyleSheet instances. When a stylesheet is added to this
array, the browser applies its rules to the corresponding DOM tree.
The Problem with Traditional Web Component Styling
Shadow DOM boundaries block external styles from penetrating custom elements to ensure encapsulation. Traditionally, developers styled Web Components using two approaches:
- Inline
<style>tags: Placing a<style>element inside thetemplateorshadowRootof every instance. - External
<link>tags: Referencing an external stylesheet within each component’s shadow root.
Both patterns have severe performance drawbacks. When thousands of component instances are rendered, the browser must parse and store identical CSS text repeatedly, leading to high memory overhead, slower initial render times, and increased layout recalculations.
How to Share Compiled CSS Using adoptedStyleSheets
Modern build pipelines (such as Webpack, Vite, or Rollup) can compile
Sass, Less, PostCSS, or Tailwind styles into a single CSS string. This
compiled CSS can then be turned into a shared CSSStyleSheet
instance and attached to any number of components.
Step 1: Create and Populate the Stylesheet
First, create a new CSSStyleSheet object and populate it
using either replace() (asynchronous) or
replaceSync() (synchronous):
// Compiled CSS imported from a build tool or defined as a string
const compiledStyles = `
:host {
display: inline-block;
font-family: system-ui, sans-serif;
}
.btn {
background-color: #0066cc;
color: #ffffff;
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.btn:hover {
background-color: #0052a3;
}
`;
// Instantiate the shared stylesheet
const sharedSheet = new CSSStyleSheet();
sharedSheet.replaceSync(compiledStyles);
export default sharedSheet;Step 2: Adopt the Stylesheet Across Components
Import the shared sheet and assign it to the
adoptedStyleSheets property of the element’s
shadowRoot:
import sharedSheet from './shared-styles.js';
class CustomButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// Share the compiled stylesheet reference
shadow.adoptedStyleSheets = [sharedSheet];
shadow.innerHTML = `<button class="btn"><slot></slot></button>`;
}
}
customElements.define('custom-button', CustomButton);If another custom element needs the same base styles along with its own unique styles, it can combine multiple sheets:
import baseSheet from './shared-styles.js';
const componentSheet = new CSSStyleSheet();
componentSheet.replaceSync(`.btn { border: 2px solid black; }`);
class OutlinedButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// Combine base styles with component-specific styles
shadow.adoptedStyleSheets = [baseSheet, componentSheet];
shadow.innerHTML = `<button class="btn"><slot></slot></button>`;
}
}
customElements.define('outlined-button', OutlinedButton);Key Advantages of Using adoptedStyleSheets
- Single Memory Allocation: The compiled CSS is parsed into an abstract syntax tree (AST) once by the browser. Multiple shadow roots simply point to that single reference in memory.
- Dynamic Updates: If you modify rules on the shared
CSSStyleSheetusing methods likeinsertRule()ordeleteRule(), the changes instantly propagate to every component instance adopting that sheet. - Composable Styles: Components can adopt an arbitrary number of stylesheets, allowing easy separation of design tokens, reset styles, utility classes, and component-specific rules.
- Seamless Build Tool Integration: Modern bundlers
support importing CSS files directly as constructable stylesheets using
CSS module scripts
(
import sheet from './styles.css' assert { type: 'css' };), making the pipeline cleaner and more automated.