Inside a shadow root's own stylesheet, what do the :host and :host-context() selectors match, and why can't the component just write its own tag name as the selector instead?
answer
- the host is not in its own tree
- a tag selector inside matches nothing
- display: inline is the default trap
- functional form reads the host's own attributes
- outer tree wins for normal declarations
basics
~20 s:host matches the shadow host from inside its shadow tree, and :host(sel) matches only when the host also matches sel. :host-context(sel) matches when the host or any ancestor matches sel. A plain tag selector fails because the host lives outside the tree.
solid answer
~50 sA stylesheet inside a shadow root can only match nodes in that tree, and the host element is not in it — it lives in the outer tree — so `my-card { display: block }` written inside the root matches nothing. `:host` is the explicit exception: it selects the shadow host from inside its own shadow tree, which is how a component gives itself a `display`, since custom elements are `display: inline` by default. `:host(.compact)` is the functional form, matching only when the host itself also matches that compound selector, so a component can react to a class or attribute set on it from outside. `:host-context(.dark)` matches when the host *or any ancestor* matches the argument, letting a component respond to context set higher up the page — but it is unevenly implemented across engines, so treat it as progressive enhancement. One cascade quirk to know: for normal declarations, a rule in the outer document that targets the host beats a `:host` rule of equal specificity.
code
css · 12 lines:host {
display: block;
box-sizing: border-box;
border: 1px solid currentColor;
}
/* restore the hidden attribute that display: block just broke */
:host([hidden]) { display: none }
/* react to a class or attribute the page sets on the host */
:host(.compact) { padding: 2px 6px }
:host([disabled]) { opacity: .5; pointer-events: none }go deeper
Remember that inside a shadow root you style the element itself with :host, not with its tag name, and that a component usually needs :host { display: block } to lay out at all.
Explain the scoping reason the tag selector fails, and distinguish the three forms: :host, the functional :host(sel) that tests the host itself, and :host-context(sel) that tests ancestors.
Show the cascade knowledge: normal declarations from the outer tree beat :host, !important reverses that, and restoring :host([hidden]) is required once you set display on the host.
Decide the contract. Choose which host attributes are a component's public styling API, and prefer explicitly passed context over :host-context() so the library behaves the same in every engine your product supports.
## Why the tag name does not work Selector matching in a shadow tree is scoped to that tree. A rule written inside the root can only match nodes in the root. The host element belongs to the *outer* tree — the shadow root hangs off it, but it is not inside it — so a bare `my-card { ... }` rule inside the root has nothing to match. Meanwhile the same rule written in the *document* stylesheet does match the host, because the host is an ordinary element out there. That asymmetry is the whole reason a dedicated selector exists. ```css /* inside the shadow root */ my-card { display: block } /* matches nothing */ :host { display: block } /* matches the host */ ``` The `display` line is not a toy example. An unknown or custom element defaults to `display: inline`, so a component that never sets `display` on itself will collapse around its content and ignore width and height. Setting it via `:host` is the standard first rule in a component's stylesheet. ## The three forms **`:host`** — matches the shadow host, but *only* when the rule appears in a stylesheet inside that host's shadow tree. Used in a document stylesheet it matches nothing. **`:host(<compound-selector>)`** — matches the host only if the host itself also matches the argument. This is how a component exposes state and variants to its consumer: ```css :host { display: inline-flex; opacity: 1 } :host([disabled]) { opacity: .5; pointer-events: none } :host(.compact) { padding: 2px 6px } ``` The page writes `<my-button disabled>` or adds a class, and the internal stylesheet reacts — no JavaScript involved. The argument is a *compound* selector applied to the host, not a descendant selector. **`:host-context(<compound-selector>)`** — matches the host if the host *or any of its ancestors* in the flattened tree matches the argument. It lets a component detect the environment it was dropped into: ```css :host-context(.dark) { background: #111; color: #eee } ``` Support for `:host-context()` is genuinely uneven — Chromium implements it, Firefox does not — so a component that relies on it alone will look wrong in some browsers. The portable alternative is to let the page hand the information down explicitly, either as an attribute on the host or as a CSS custom property inherited from an ancestor. ## Specificity and the cascade Two details trip people up. *Specificity.* `:host` counts as a pseudo-class, so it has specificity (0,1,0). `:host(.compact)` adds the compound's specificity, giving (0,2,0). Nothing exotic — it competes with ordinary selectors on ordinary terms *within the same tree*. *Cross-tree cascade.* When a declaration from the outer document and a declaration from the shadow tree both apply to the host and neither is `!important`, **the outer document wins**, regardless of specificity. That is the platform deliberately letting the page have the last word about the element it placed. Flip on `!important` and the order reverses: an important declaration from the shadow tree beats an important one from the document. Concretely: ```css /* document */ my-card { color: blue } /* shadow root */ :host { color: red } /* result: blue */ ``` Candidates who expect `:host` to win here are reasoning from specificity alone and missing the cross-tree rule. ## Practical shape of a component stylesheet A well-formed component stylesheet usually opens with the host block and then styles internals: ```css :host { display: block; box-sizing: border-box; } :host([hidden]) { display: none } /* restore the hidden attribute */ .body { padding: 12px } ``` The `:host([hidden])` line matters more than it looks: once you set `display: block` on `:host`, you have overridden the user-agent rule that makes `[hidden]` work, and the element will stay visible when the page sets `hidden`. Restoring it is a small, very commonly missed correctness detail — and a good thing to volunteer in an interview, because it shows you have actually shipped a component rather than read about one. ## What these selectors do not do `:host` styles the host itself; it does not reach the component's internals from outside, and it does not give the page a way in. Styling that crosses the boundary in the other direction is a separate mechanism entirely. Keep the direction straight: `:host` is a component looking *outward at itself*, from inside its own tree.
- After you write :host { display: block }, the page sets the hidden attribute and the component stays visible. Why?The user-agent stylesheet implements `hidden` with a low-priority `display: none`, and your `:host` rule overrides it for this element. Add `:host([hidden]) { display: none }` to the component's stylesheet so the attribute keeps working. Every component that sets `display` on its host owes this line.
- A document rule and a :host rule both set color on the host, neither is !important. Which one applies?The document's. For normal declarations the outer tree wins over the shadow tree regardless of specificity, so the page keeps the final say over the element it wrote. Marking the shadow declaration `!important` reverses it — for important declarations the inner tree wins.
- Why do teams avoid depending on :host-context() for light/dark handling?Implementation is uneven — Chromium ships it, Firefox does not — so a component built on it renders wrongly in browsers that ignore the rule. The portable approach is to have the page pass the context down explicitly, as an attribute on the host that `:host([theme=dark])` can match, or through an inherited value the component reads.
- Does :host do anything when written in a document stylesheet?No. `:host` only matches when the rule lives in a stylesheet inside that host's shadow tree; from the document it matches nothing. From outside you address the element by its ordinary tag, class or attribute selectors, exactly like any other element in the page.
saying these in an interview costs you the question
- Writes the custom element's tag name inside its own shadow stylesheet
- Assumes :host beats the document because of specificity
- Expects :host-context() to work in every browser
- Forgets custom elements default to display: inline
- Thinks :host can select the component's internal nodes