CSS and Theming

So far, in discussions about shadow DOM and components, we've focused on the isolation that it creates for styles. We know that we can put an inline stylesheet in the top of our shadow root, and the only properties that will pass between shadow and light (or vice versa) are inherited properties. But complete isolation is in many ways as bad as no isolation — otherwise, we'd build everything in iframes. Ideally, we want to be able to poke some holes through the isolation in both directions, so that we can offer options for theming our components, and control the element itself without requiring users to load a second stylesheet.

Our portal for breaking through the boundary is the :host pseudo-class, which can be used from inside the shadow DOM to refer to the element that owns that shadow root.

/* selects the host element itself */
:host {
  /* custom elements start display: inline, which is awkward
     it's a good practice to make them block to start,
     then nest other layout elements like flex/grid inside */
  display: block;
}

:host can also be used with parentheses to select shadow elements based on a rule for the host. For example, we might show controls on a media element only if the matching attribute is present:

.controls-container {
  display: none;
}

:host([controls]) .controls-container {
  display: block;
}

The important thing about the basic host selector is that it's extremely low specificity — like user-agent styles, they're easy to override. We can use this to set the default styling for our tag without requiring developers to add a lot of !important to the outer styles. But we can also use it to set CSS variables for the styles in our shadow DOM.

:host {
  --background: white;
  --color: blue;
}

/* buttons will be colored accordingly */
button {
  background: var(--background);
  color: var(--color);
}

Using the :host() form lets us set colors based on component attributes. This is useful for creating on-off switches (like the "controls" example) or simple themes (like a force-toggle for dark mode).

/* this inverts our theme if our element has a "dark" attribute */
:host([dark]) {
  --background: blue;
  --color: white;
}

But here's the real trick: CSS variables are actually inherited properties, just like font-family or color. The :host is enough to assign them for the inside of the component, but styles set with even the smallest specificity outside will easily overcome that rule. The easiest way to do this is to write rules for our custom element's tag name in the outer stylesheet:

custom-element {
  --background: red;
  --color: black;
}

Effectively, this lets you poke specific holes in the shadow DOM boundary — obviously, you can use CSS variables to set colors for your UI, but you can also define values for layout and additional styling:

Now, key to this is that, while a few CSS variables can do a surprising amount when combined with calc() (or clamp(), min(), max(), etc.), these are very specific adjustments that are made possible. Ultimately, that's a good thing, as it lets you choose which adjustments are safe to allow and which ones will break your component. But it does mean that these theming options must be very clearly documented and communicated to developers. If you're working on multiple components, it may be helpful to pick a namespace or a common set of variable names, to make them easy to remember or define across the page.