skip to content

In a component's shadow-root CSS, what can the `::slotted()` pseudo-element actually select, and why do so many attempts to style slotted content with it fail?

level: middleimportance: should knowfreq 38%

answer

  1. assigned top-level nodes only
  2. one compound selector, no combinators
  3. text nodes are not matched
  4. outer tree wins the cascade
  5. style the slot for inherited values

basics

~20 s

::slotted() matches only the top-level nodes assigned to a slot, and takes a single compound selector — no combinators, no descendants. It also loses to the page's own rules on those elements, because slotted nodes still belong to the document tree.

solid answer

~40 s

Slotted content is not part of the shadow tree — the nodes stay in the light DOM and are only rendered in the slot's position. `::slotted()` is the narrow hole the spec opens: it matches **directly assigned top-level nodes** and its argument must be a single compound selector, so `::slotted(p)` and `::slotted(.item.wide)` work while `::slotted(div p)` is invalid and `::slotted(li) span` matches nothing. Two further surprises: it matches elements only, never assigned text nodes, and its declarations are **weak** — under shadow cascade order the page's own `.item { color: black }` beats the shadow's `::slotted(.item) { color: red }` for normal declarations, since the outer tree wins. `!important` inside the shadow flips that. The practical consequence is that `::slotted()` sets defaults for consumer markup, it does not control it.

code

css · 9 lines
css
/* inside the shadow root */
slot { display: block; font-style: italic; } /* inherits into slotted nodes */

::slotted(p)      { margin: 0 0 8px; }       /* top-level <p> only */
::slotted(.badge) { --badge-fg: #b00; }      /* hand a token outward */

/* invalid or useless — common mistakes */
/* ::slotted(div p) {}   invalid: combinator inside the argument */
/* ::slotted(li) span {} matches nothing: cannot descend past it   */

go deeper

for a junior

Know that ::slotted() is how a component's shadow CSS touches content the page passed in, and that it only reaches the top-level nodes that were slotted, not anything nested inside them.

for a middle

Explain the three limits — compound selector only, no descending, elements not text — and the cascade rule that lets the page's own styles override ::slotted() declarations.

for a senior

Demonstrate the workaround thinking: set inherited properties and custom properties on the slot, control layout with a wrapper you own, and reserve !important for values the component genuinely cannot let vary.

for a principal

Frame it as an ownership boundary: the page authored the slotted markup and keeps the last word on it. Decide what your library defends with !important, what it merely defaults, and how that contract is documented for consumers.

## Slotted nodes never leave the light DOM When a consumer writes `<my-card><p class="note">Hi</p></my-card>`, the `<p>` is a child of the host in the document tree. Slotting does not move it; the flattened tree just renders it at the slot's position. That single fact explains nearly every surprise here: the `<p>` is still a document-tree element, so document rules match it normally, and shadow-tree rules must use a special pseudo-element to reach it at all. ## What ::slotted() matches ```css /* inside the shadow root */ ::slotted(p) { margin: 0; } ::slotted(.note) { color: #555; } slot[name="header"]::slotted(*) { font-weight: 600; } ``` Three constraints: **Top level only.** It matches nodes *assigned* to the slot — the host's direct children with a matching `slot` attribute. Their descendants are unreachable. In `<my-card><p>Hi <em>there</em></p></my-card>`, `::slotted(p)` matches the paragraph and nothing in the shadow tree can select the `<em>`. **Compound selector only.** The argument is one compound selector: a tag, classes, attributes, pseudo-classes on the same element. `::slotted(div p)`, `::slotted(div > p)` and `::slotted(.a, .b)`-style structural nesting are not valid arguments (a selector list of compounds is allowed, combinators are not). Anything requiring a relationship between two elements is out. **No descending after it.** `::slotted(li) span` matches nothing, for the same reason as `::part()`: it is a pseudo-element. **Elements only.** Bare text assigned to a slot — `<my-card>Hello</my-card>` — is a text node, and `::slotted(*)` does not match it. Style it by styling the `slot` element itself, since the text inherits from where the slot sits. ## Why the page usually wins The subtler failure is a rule that *does* match but has no effect. Under the CSS scoping rules, when declarations from two different trees apply to the same element, **tree order decides before specificity**: for normal declarations, the outer tree wins. Slotted elements belong to the outer tree, so the consumer's plain `.note { color: black }` beats the shadow's `::slotted(.note) { color: red }` even though the shadow selector looks more specific. That is deliberate — the consumer authored that markup and should keep control of it — but it catches component authors who expected their shadow CSS to be authoritative. `!important` in the shadow tree reverses the order and wins, and is the right tool only for things that genuinely must not vary (a `display` value the layout depends on, for instance). ## Styling the slot instead A `<slot>` is a real element in the shadow tree and can be styled directly: ```css slot { display: block; padding: 8px; } ``` But a slot's default `display` is `contents`, so it generates no box of its own; setting `display: block` makes it a real box that wraps the assigned nodes, which changes layout and is sometimes what you want and sometimes a surprise. Inherited properties set on the slot *do* reach slotted content — the flattened tree parents the assigned nodes to the slot — so `slot { font-style: italic }` italicises them even though no `::slotted()` rule matched. That is often the cleanest way to influence slotted content: set inherited properties (and custom properties) on the slot or its shadow ancestors. ## What to do when ::slotted() is not enough - **Push values outward.** Declare custom properties on the slot's shadow ancestors; slotted content inherits them, and the consumer's own CSS can consume them with `var()`. - **Constrain by layout, not by selector.** Wrap the slot in a container you fully control and style that: padding, grid placement, max-width all work without touching consumer markup. - **Accept the ownership split.** The consumer authored those nodes; deep-styling them from inside would break the mental model that light DOM belongs to the page. `::slotted()` is best read as a way to set *defaults* — margin resets, a base colour — that consumers may freely override.

  • A shadow rule `::slotted(.note) { color: red }` is ignored because the page has `.note { color: black }`. Why, and what are the options?
    Shadow cascade order puts the outer tree first for normal declarations, and slotted nodes belong to the outer tree, so the page wins regardless of specificity. Options: accept it as the correct ownership split, add `!important` in the shadow if the value is genuinely non-negotiable, or move the styling to an element you own — a wrapper around the slot — instead of the consumer's node.
  • How do you style plain text passed into a slot, as in `<my-card>Hello</my-card>`?
    `::slotted()` matches elements only, so it never sees a text node. Style the `<slot>` element or one of its shadow ancestors with inherited properties instead — `slot { font-style: italic; color: #555 }` — because assigned nodes inherit from the slot's position in the flattened tree.
  • Does setting `display: block` on a `<slot>` change anything visually?
    Yes. A slot defaults to `display: contents`, generating no box, so assigned nodes participate directly in the surrounding layout. Giving it `display: block` creates a real box that wraps them, which can change flex/grid participation and margin collapsing. Wrapping the slot in a `<div>` you control is usually clearer than restyling the slot itself.

saying these in an interview costs you the question

  • ::slotted can reach descendants of slotted elements
  • Slotted nodes become part of the shadow tree
  • Higher specificity beats the page's rules on slotted nodes
  • ::slotted(*) styles assigned text nodes too
  • ::slotted(div p) is a valid selector

context