Skip to content

Custom Properties and Constructable Stylesheets

CSS custom properties (also called CSS variables) are the primary theming API for web components. Unlike regular CSS rules, custom properties inherit through shadow boundaries. The host element or any ancestor can declare --color: blue, and the shadow root can read it with color: var(--color, defaultValue). The second argument is the fallback, used when the property is not set.

/* Page or host sets the token */
themed-badge {
--badge-bg: #7c3aed;
--badge-color: white;
}
/* Shadow root reads it */
span {
background: var(--badge-bg, #6b7280);
color: var(--badge-color, white);
}

This inheritance is intentional: custom properties were designed to punch through encapsulation. Everything else (selectors, cascade) stops at the shadow boundary, but -- variables flow freely.

Constructable Stylesheets let you create a CSSStyleSheet object in JavaScript, populate it once with .replaceSync(), and then share that single object across many shadow roots by assigning it to shadowRoot.adoptedStyleSheets. Each shadow root that adopts the sheet gets the styles without duplicating the parsed CSS in memory.

const sheet = new CSSStyleSheet();
sheet.replaceSync('span { font-weight: bold; color: steelblue; }');
class MyEl extends HTMLElement {
connectedCallback() {
var shadow = this.attachShadow({ mode: 'open' });
shadow.adoptedStyleSheets = [sheet];
}
}

This is particularly useful in design systems where dozens of components share a common base stylesheet. Parse once, adopt everywhere.

The demo below defines a <themed-badge> element whose background and text color are driven entirely by CSS custom properties set on the host from the page stylesheet. Change the --badge-bg value in the CSS panel to retheme without touching the component code.

OptionBenefitCost
CSS custom propertiesA simple, flexible theming channel that inherits through the shadow boundary with no JavaScript neededThe outside page can’t override every internal style decision — only the tokens the component author chose to expose
Constructable StylesheetsShares a single CSSStyleSheet object across many shadow roots, saving memory and avoiding re-parsingRequires extra JavaScript to create and adopt the sheet — more complex than just dropping a <style> tag into each instance
  • Duplicating a <style> tag in every instance — repeating the same style block in every shadow root instead of creating one CSSStyleSheet and sharing it via adoptedStyleSheets, wasting memory and making style updates harder.
  • Forgetting a fallback value — writing var(--badge-bg) with no fallback means the component silently breaks when the property isn’t set. Always write var(--badge-bg, #6b7280).
  • Expecting custom properties to override everything — custom properties are only the tokens the component author chose to expose via var(); they don’t give the page permission to override the shadow root’s internal structure or layout.

💡 Real-world examples

Shoelace — exposes a consistent CSS custom-property API across its entire component set, so consumers can adjust colors and spacing for every component from one place without touching the internal shadow DOM.

Lit — Lit’s css tagged template creates a CSSStyleSheet shared at the class level, then adopts it into every instance’s shadow root automatically — exactly the Constructable Stylesheets idea covered in this lesson.

Do CSS custom properties (--vars) cross the shadow DOM boundary?
What is the syntax to provide a fallback for a custom property inside a shadow root?
What does adoptedStyleSheets on a shadow root allow?