Slots project light-DOM children into a component’s shadow tree. This reference covers the <slot>
element, named slots, the slotchange event, the assignedNodes/assignedElements APIs,
::slotted() styling and the attachShadow options.
How it works
A component places <slot> elements in its shadow root. The browser then assigns the host’s
light-DOM children into those slots to form the flattened tree that is actually rendered:
<my-card>
<h2 slot="title">Hello</h2> <!-- → <slot name="title"> -->
<p>Body text</p> <!-- → default <slot> -->
</my-card>
Inside the shadow root:
<div class="head"><slot name="title"></slot></div>
<div class="body"><slot></slot></div>
Children with a matching slot="..." attribute go to the named slot; everything else goes to the
single default (unnamed) slot. Unmatched children render nothing. A slot’s own contents act as
fallback shown only when no nodes are assigned.
Reacting and styling
- Listen for
slotchangeon a slot to know when its assigned nodes change. It fires for assignment changes, not for mutations inside an already-assigned node. - Read what is assigned with
slot.assignedElements({ flatten: true })(elements only) orassignedNodes()(includes text/comments). - Style top-level projected nodes from the shadow root with
::slotted(selector)— it cannot reach descendants of assigned nodes. attachShadow({ mode })controls whetherelement.shadowRootis exposed (open) ornull(closed);delegatesFocus: trueforwards focus to the first focusable child.
Common patterns and gotchas
Default content fallback
A slot can carry fallback markup that renders only when no light-DOM node is assigned to it. This is useful for placeholder text or a default icon without requiring the component consumer to provide anything:
<slot name="icon">
<svg><!-- default icon --></svg>
</slot>
Once any node with slot="icon" is slotted in, the fallback disappears completely.
Detecting slotchange correctly
A common mistake is expecting slotchange to fire when the content inside a slotted element
changes — for example, updating the text of an already-assigned <p>. It does not. The event fires
only when the set of assigned nodes on the slot changes. To react to deeper mutations you need a
MutationObserver on the assigned elements themselves, obtained via assignedElements().
The flatten option
When a custom element nests inside another custom element that also uses slots, a slot can be
reassigned through multiple levels of shadow trees. Calling assignedNodes({ flatten: true }) walks
that chain and returns the actual light-DOM leaf nodes, rather than the intermediate slot elements.
Without flatten: true you get the intermediate slots when slots are composed, which is rarely
what you want.
::slotted limitations
The ::slotted() pseudo-element can match only the top-level assigned node — it cannot
pierce down into a slotted element’s own children. So ::slotted(p span) will not work; you can
write ::slotted(span) and it will match only spans assigned directly to the slot, not spans nested
inside a slotted <div>. For deeper styling, use CSS custom properties or parts as a bridge between
the shadow and light trees.
open vs closed in practice
mode: 'closed' hides the shadowRoot from element.shadowRoot, but any code that runs inside
the element’s class constructor still has the reference. Frameworks and testing libraries that
rely on shadowRoot access can be blocked by closed mode, so weigh the friction against the
modest privacy it provides before choosing it.
Quick member checklist
| Member | What it does |
|---|---|
<slot> | Default slot — receives unmatched light-DOM children |
<slot name="x"> | Named slot — receives children with slot="x" |
slotchange event | Fires when assigned nodes on the slot change |
assignedNodes() | Returns assigned nodes including text and comment nodes |
assignedElements() | Returns only assigned Element nodes |
{ flatten: true } | Resolve nested slot composition to leaf nodes |
::slotted(sel) | Style top-level projected nodes from within shadow root |
attachShadow({ mode }) | open exposes .shadowRoot; closed returns null |
delegatesFocus: true | Forwards focus to first focusable element in the shadow |