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

  1. 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.
  2. Live Dynamic Updates: Because custom elements share the same object reference, mutating the stylesheet (for example, via sharedStyles.insertRule() or sharedStyles.replaceSync()) instantly updates the styling across all elements that adopt that sheet.
  3. Style Composition: adoptedStyleSheets is 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.