skip to content

A custom element attaches a shadow root and renders <span class="label">Hi</span> inside it. Why does the page's `body { font-family: Georgia }` change that text, while the page's `body .label { color: red }` does nothing?

level: juniorimportance: must knowfreq 55%

answer

  1. two mechanisms, not one
  2. selectors stop, values still flow
  3. the inherited-property flag decides
  4. host element is the doorway
  5. resets do not reach inside

basics

~20 s

Inheritance crosses a shadow boundary; selector matching does not. font-family is an inherited property, so its computed value flows from the host down into the shadow tree. A page selector can never match a node inside another tree's shadow root.

solid answer

~50 s

Two separate mechanisms are at play. **Selector matching is scoped per tree**: a rule in the document stylesheet only matches nodes in the document tree, so `body .label` cannot see a `.label` that lives inside a shadow root, and the same is true in reverse. **Inheritance is computed over the flattened tree**, and the shadow tree's top-level nodes inherit from the host element. So every *inherited* property — `color`, `font-family`, `line-height`, `letter-spacing`, `visibility`, `cursor`, `list-style` — keeps flowing inward, while non-inherited ones like `background`, `border`, `padding` or `box-sizing` stop at the host. Open vs closed mode changes nothing about CSS; mode only affects JavaScript access to the root. That is why a global reset like `* { box-sizing: border-box }` silently fails to apply inside components, and why themeable hooks have to be built deliberately — with custom properties or `part` names.

go deeper

for a junior

Be able to say plainly that inherited properties like color and font-family reach into a shadow root while page selectors do not match anything inside it. Naming a couple of properties on each side is enough here.

for a middle

Explain the mechanism: selector matching is per tree, inheritance is computed over the flattened tree where the host is the parent. Mention that a universal-selector reset therefore never applies inside a shadow root.

for a senior

Show the production consequence — components that inherit page typography by design, and components that must reset at :host to survive hostile host pages. Be ready to weigh all: initial against re-declaring a few properties.

for a principal

Own the library-wide stance: does your component set inherit the host page's typography (cheap integration, unpredictable rendering) or reset at the boundary (consistent, but every consumer theme now needs explicit hooks)? Say which you would pick and what it costs consumers.

## Two mechanisms that people collapse into one "Shadow DOM encapsulates styles" is true, but it describes only half of what happens. There are two independent things CSS does, and the shadow boundary treats them differently. **Selector matching** is scoped to a tree. A style rule belongs to some tree — the document, or a particular shadow root — and its selectors are evaluated only against elements in that same tree. A document rule cannot reach into a shadow root, and a shadow root's rules cannot reach out into the document (with the deliberately-sanctioned exceptions `:host`, `::slotted()` and `::part()`). **Inheritance** is not selector matching. It is the step where an element that has no cascaded value for an inherited property takes its parent's computed value. That step runs over the *flattened tree*, which stitches shadow trees into the document: a shadow root's top-level children inherit from the host element, and slotted light-DOM nodes inherit from wherever the slot sits in the shadow tree. Nothing about encapsulation blocks it. So the page's `body { font-family: Georgia }` never "matches" the `<span class="label">` inside the shadow root. It matches `<body>`, and the value is then inherited down through the host and into the shadow tree, arriving at the span with no rule ever having selected it. ## Which properties cross The dividing line is exactly the CSS spec's inherited flag, nothing shadow-specific: - **Cross by inheritance**: `color`, `font` and all its longhands, `line-height`, `letter-spacing`, `word-spacing`, `text-align`, `text-transform`, `white-space`, `visibility`, `cursor`, `direction`, `list-style`, and — importantly — every **custom property** (`--*`). - **Do not cross**: `display`, `background`, `border`, `padding`, `margin`, `width`/`height`, `position`, `box-shadow`, `overflow`, `box-sizing`, `opacity`, `transform`. ```css /* document stylesheet */ body { font-family: Georgia; color: #333; } * { box-sizing: border-box; } /* never applies inside a shadow root */ my-card .label { color: red; } /* matches nothing */ ``` The span ends up Georgia and `#333`, with `box-sizing: content-box`, because the universal selector rule matched only document-tree elements. ## The reset-stylesheet trap This is the most common real-world bite. Teams ship a normalize/reset stylesheet and assume it governs the whole page. Inside every shadow root, none of it applies: margins on `<h1>`, the default `box-sizing`, button font inheritance quirks all revert to UA defaults. A component that looked fine in a demo page (which had a reset and no shadow root) can shift once its markup moves inside a shadow root. Component authors therefore ship their own small reset inside the shadow root rather than relying on the host page's. ## Open vs closed is irrelevant here `attachShadow({ mode: 'open' })` versus `'closed'` only decides whether `element.shadowRoot` returns the root to script. Style encapsulation, inheritance, `::part()` and `::slotted()` behave identically in both modes. A candidate who says page CSS can select into an open root but not a closed one has the model wrong. ## Controlling the inheritance you get for free Sometimes the free inheritance is what you want — a component picks up the page's typography automatically and looks native. Sometimes it is contamination: the page sets `line-height: 3` and your compact toolbar sprawls. The blunt tool is to reset at the doorway: ```css :host { all: initial; /* stops inherited values from the page */ display: block; /* all: initial reset display too, so re-declare */ } ``` `all: initial` on `:host` wipes every inherited property at the host, so nothing arrives from outside. It is heavy-handed: it also resets `display`, so you must set it again, and you lose the "blends into the page" property. A gentler approach is to re-declare only the handful you care about (`font: 16px/1.4 system-ui;`) and let the rest inherit. ## What this leaves for theming Because selectors do not cross, a page cannot restyle a component's internals by writing selectors, no matter how specific. The three sanctioned ways in are: inherited properties (free, but limited to what inherits), **custom properties** the component author wired into `var()` calls, and **`part` names** the author exposed for `::part()`. Everything else is intentionally private — which is the whole point, and also the reason a component with no theming hooks is effectively unstylable.

  • How would you stop the host page's typography from leaking into your component's shadow tree?
    Re-declare the inherited properties at the doorway. `:host { all: initial }` resets every inherited value arriving from outside, but it also clears `display`, so you must re-declare `display: block` (or whatever the component needs) plus the fonts you do want. A lighter version is to just set `font: 16px/1.4 system-ui; color: #111;` on `:host` and let the rest inherit.
  • Name properties people assume are inherited but that stop at the host.
    `box-sizing` is the big one — global resets set it with a universal selector, and neither the selector nor the value reaches inside. Also `background`, `border`, `padding`, `margin`, `display`, `overflow`, `opacity`, `transform` and `box-shadow`. Anything layout- or box-related is non-inherited by design; only text-ish properties and custom properties travel.
  • Does the mode passed to attachShadow change any of this?
    No. `open` and `closed` differ only in whether `element.shadowRoot` returns the root to JavaScript. CSS encapsulation, inheritance through the flattened tree, `::part()` and `::slotted()` behave identically in both. Closed mode is not a stronger style boundary, and it is not a security boundary either.

saying these in an interview costs you the question

  • Shadow DOM blocks all outside styles, including fonts
  • Page selectors reach inside if the root is open
  • Closed mode is what actually blocks page CSS
  • Global resets apply everywhere, shadow roots included
  • Only custom properties can cross the boundary

context