How to Use adoptedStyleSheets in Custom Elements
The adoptedStyleSheets property provides a standardized,
performant way to share and apply CSS across multiple Shadow DOM roots
and documents using Constructable Stylesheets. Instead of repeatedly
injecting and parsing duplicate <style> tags inside
every custom element instance, developers can create a single CSS
stylesheet object in JavaScript and assign it directly to multiple
shadow roots. This approach optimizes memory usage, improves rendering
performance, and enables centralized style management across Web
Components.
The Problem with Traditional Styling in Web Components
Traditionally, encapsulation in Shadow DOM required embedding
<style> elements within the template of each custom
element:
class MyButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
button { background: blue; color: white; }
</style>
<button><slot></slot></button>
`;
}
}When rendering hundreds or thousands of instances of
MyButton, the browser must parse and store the identical
CSS string repeatedly in memory. This leads to increased memory overhead
and slower initial render times.
How Constructable Stylesheets and adoptedStyleSheets Work
Constructable Stylesheets solve this limitation by allowing you to
instantiate a CSSStyleSheet object directly via JavaScript
using new CSSStyleSheet(). Once created, styles are added
programmatically using either the asynchronous replace()
method or the synchronous replaceSync() method.
The resulting stylesheet object is then added to the
adoptedStyleSheets array of any Document or
ShadowRoot.
Implementation Example
// 1. Create and define the shared stylesheet once
const sharedStyles = new CSSStyleSheet();
sharedStyles.replaceSync(`
:host {
display: inline-block;
font-family: sans-serif;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
border: none;
cursor: pointer;
}
.primary {
background-color: #007bff;
color: white;
}
`);
// 2. Share the stylesheet across different custom elements
class PrimaryButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sharedStyles];
shadow.innerHTML = `<button class="btn primary"><slot></slot></button>`;
}
}
class SecondaryButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// Multiple stylesheets can be composed together
shadow.adoptedStyleSheets = [sharedStyles];
shadow.innerHTML = `<button class="btn"><slot></slot></button>`;
}
}
customElements.define('primary-button', PrimaryButton);
customElements.define('secondary-button', SecondaryButton);Key Advantages of adoptedStyleSheets
- Memory Efficiency: The browser parses the CSS rules only once. Every element adopting the stylesheet holds a reference to the same underlying object rather than maintaining its own copy.
- Live Dynamic Updates: Because custom elements share
the same object reference, mutating the stylesheet (for example, via
sharedStyles.insertRule()orsharedStyles.replaceSync()) instantly updates the styling across all elements that adopt that sheet. - Style Composition:
adoptedStyleSheetsis an array. This allows custom elements to combine multiple modular stylesheets, such as a global design system token sheet, a component-specific sheet, and a theme override sheet:
shadowRoot.adoptedStyleSheets = [themeStyles, baseComponentStyles];By leveraging adoptedStyleSheets, JavaScript custom
elements maintain strict encapsulation boundaries while drastically
reducing resource consumption and simplifying style management.