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
Section titled “Constructable Stylesheets”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.
Live Demo
Section titled “Live Demo”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.
Trade-offs
Section titled “Trade-offs”| Option | Benefit | Cost |
|---|---|---|
| CSS custom properties | A simple, flexible theming channel that inherits through the shadow boundary with no JavaScript needed | The outside page can’t override every internal style decision — only the tokens the component author chose to expose |
| Constructable Stylesheets | Shares a single CSSStyleSheet object across many shadow roots, saving memory and avoiding re-parsing | Requires extra JavaScript to create and adopt the sheet — more complex than just dropping a <style> tag into each instance |
Common mistakes
Section titled “Common mistakes”- Duplicating a
<style>tag in every instance — repeating the same style block in every shadow root instead of creating oneCSSStyleSheetand sharing it viaadoptedStyleSheets, 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 writevar(--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
csstagged template creates aCSSStyleSheetshared at the class level, then adopts it into every instance’s shadow root automatically — exactly the Constructable Stylesheets idea covered in this lesson.