Shared styles without the duplication tax

Creating stylesheets from JavaScript has always been possible, but the traditional route—spinning up a <style> element with document.createElement('style') and then reaching into its sheet property—has real costs. Duplicate CSS creeps in, and the act of attaching styles can trigger a flash of unstyled content. Constructable Stylesheets aims to fix that by working directly with the CSSStyleSheet interface, the root of the CSS Object Model (CSSOM).

With this API, you can define shared CSS once and apply it to multiple Shadow Roots or the Document itself, without duplication. When a shared stylesheet is updated, every context that adopted it sees the change. Adoption is fast and synchronous, provided the sheet is already loaded.

The model opens up several practical patterns: a centralized theme passed as a CSSStyleSheet instance to many components, distribution of CSS Custom Property values to specific subtrees without relying on the cascade, or even a direct, parser-backed way to preload stylesheets before they touch the DOM.

Constructing a stylesheet

The spec extends the existing CSSStyleSheet constructor rather than inventing a new interface. Once you have an instance, two new methods—replace() and replaceSync()—let you swap in CSS safely, without triggering flash of unstyled content. Both take a string of CSS; replace() returns a Promise. External references are not honored here: any @import rules are ignored and generate a warning.

const sheet = new CSSStyleSheet();

// replace all styles synchronously:
sheet.replaceSync('a { color: red; }');

// replace all styles:
sheet.replace('a { color: blue; }')
  .then(() => {
    console.log('Styles replaced');
  })
  .catch(err => {
    console.error('Failed to replace styles:', err);
  });

// Any @import rules are ignored.
// Both of these still apply the a{} style:
sheet.replaceSync('@import url("styles.css"); a { color: red; }');
sheet.replace('@import url("styles.css"); a { color: red; }');
// Console warning: "@import rules are not allowed here..."

Adopting stylesheets into a tree

The second piece of the puzzle is an adoptedStyleSheets property on both Shadow Roots and Documents. Assigning an array of CSSStyleSheet objects to this property explicitly applies their rules to that DOM subtree.

// Create our shared stylesheet:
const sheet = new CSSStyleSheet();
sheet.replaceSync('a { color: red; }');

// Apply the stylesheet to a document:
document.adoptedStyleSheets.push(sheet);

// Apply the stylesheet to a Shadow Root:
const node = document.createElement('div');
const shadow = node.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets.push(sheet);

The result is a clear, imperative path for creating stylesheets and wiring them into the DOM. The Promise-based loading API leans on the browser's built-in CSS parser, and because the same stylesheet object can serve many roots, updates—theme shifts, preference changes, and the like—propagate automatically everywhere the sheet is used.

What's next

The initial release covers the API described above. Work is already underway to smooth out usability: a proposal exists to add dedicated insert and remove methods to the adoptedStyleSheets array, which would remove the need for array cloning and reduce the chance of duplicate sheet references.