Educational guide to custom element and Shadow DOM selectors
Where each selector can act
Compare the style sheet containing the rule, the subject receiving the style, and the encapsulation boundary for each form.
State and host
The custom element hosts its shadow root. An internal rule uses :host or :host-context(); a page rule can use the element name and :state(active).
Host
:hostSelects the host from a stylesheet inside its shadow root.
Specificity(0, 1, 0)
Host
:hostBehavior
- The internal rule can style the custom element itself.
- It does not require a class, attribute, or custom state on the host.
Examples
The :host rule belongs inside the shadow root; the HTML shows the host on the page.
:host<baseline-shadow-card>
</baseline-shadow-card>Receives the style: Custom element host
Common mistakes
- Writing :host in a page stylesheet is not equivalent to an internal rule.
- The host is not an ordinary shadow-root descendant for arbitrary selectors.
Host class
:host(.selected)Selects the host when it has the selected class.
Specificity(0, 2, 0)
Host class
:host(.selected)Behavior
- The :host() argument filters the host itself.
- The class appears in HTML and is separate from ElementInternals.states.
Examples
The HTML includes class="selected" on the host; the rule belongs inside the shadow root.
:host(.selected)<baseline-shadow-card class="selected">
</baseline-shadow-card>Receives the style: Custom element host
Common mistakes
- Confusing .selected with :state(active): they are independent conditions.
- Putting the class on an internal child does not satisfy :host(.selected).
Host custom state
:host(:state(active))Selects the host with the active custom state from its internal sheet.
Specificity(0, 2, 0)
Host custom state
:host(:state(active))Behavior
- The component activates the state with ElementInternals.states.add("active").
- The state is not serialized as a class or attribute in the displayed HTML.
Examples
HTML alone cannot express this state: the generated JS calls setActive(true) before checking the internal rule.
:host(:state(active))<baseline-shadow-card>
</baseline-shadow-card>Receives the style: Custom element host
Common mistakes
- Looking for an active attribute in HTML to explain :state(active).
- Treating syntax acceptance as proof that the rule applied a style.
Support and compatibility
Check the Baseline status of :state() and follow its link for browser details. The playground tests whether this internal rule applies a style here.
State from the page
baseline-shadow-card:state(active)Selects the custom element with the active state from the page.
Specificity(0, 1, 1)
State from the page
baseline-shadow-card:state(active)Behavior
- The external rule reaches the host without entering its shadow root.
- It observes the same CustomStateSet used by internal :host(:state(active)).
Examples
The generated JS activates the host state; this CSS rule belongs to the page.
baseline-shadow-card:state(active)<baseline-shadow-card>
</baseline-shadow-card>Receives the style: Custom element host
Common mistakes
- Assuming the page rule can select internal nodes because the host is active.
- Replacing custom state with a class and presenting it as the same test.
Support and compatibility
This form uses :state() in the page stylesheet. The card shows its Baseline status; the playground tests the style in this browser.
Host context
:host-context(.theme-active)Selects the host from its internal sheet when it or an outer ancestor has the theme-active class.
Specificity(0, 2, 0)
Host context
:host-context(.theme-active)Behavior
- The condition checks the context surrounding the host, although the CSS rule lives inside the shadow root.
- The host remains the subject receiving the style.
Examples
The outer div has theme-active; :host-context(.theme-active) belongs inside the shadow root.
:host-context(.theme-active)<div class="theme-active">
<baseline-shadow-card>
</baseline-shadow-card>
</div>Receives the style: Custom element host
Common mistakes
- Writing :host-context() in the page stylesheet.
- Treating a match as permission to select arbitrary internal nodes from the page.
Support and compatibility
Availability is still limited. Follow the card link for browser details and test the effect of theme-active in the playground.
Slots and parts
A slot projects page children; :has-slotted checks whether it received content. A part exposes an internal surface for the page to style.
Assigned element
slot[name="content"]::slotted(.item)Selects the .item element directly assigned to the content slot.
Specificity(0, 2, 2)
Assigned element
slot[name="content"]::slotted(.item)Behavior
- The rule belongs inside the shadow root, next to the slot.
- The subject is an assigned light-DOM child; the second HTML block shows the internal slot.
Examples
The span belongs to page HTML and is distributed to the content slot in the shadow root.
slot[name="content"]::slotted(.item)<baseline-shadow-card>
<span slot="content" class="item">Assigned content</span>
</baseline-shadow-card><div part="panel">
<slot name="content"></slot>
</div>Receives the style: Element assigned to the slot
Common mistakes
- Expecting ::slotted(.item) to reach every descendant of the assigned element.
- Using document.querySelectorAll() counts as if they evaluated the internal rule.
Slot with content
slot[name="content"]:has-slottedSelects the content slot when it has assigned nodes.
Specificity(0, 2, 1)
Slot with content
slot[name="content"]:has-slottedBehavior
- The rule lives inside the shadow root and styles the slot itself.
- Assigned text nodes can also satisfy the condition; this demo uses a span.
Examples
The page span is assigned to the internal slot; removing it makes the slot stop matching :has-slotted.
slot[name="content"]:has-slotted<baseline-shadow-card>
<span slot="content" class="item">Assigned content</span>
</baseline-shadow-card><div part="panel">
<slot name="content"></slot>
</div>Receives the style: Slot inside the shadow root
Common mistakes
- Confusing :has-slotted with ::slotted(.item): they select different subjects.
- Assuming the rule selects the assigned span rather than the slot.
Support and compatibility
Availability is still limited. Follow the card link for browser details and test the slot with and without assigned content.
Exposed part
baseline-shadow-card::part(panel)Styles the internal panel exposed as part="panel" from the page.
Specificity(0, 0, 2)
Exposed part
baseline-shadow-card::part(panel)Behavior
- The component explicitly declares which internal element it exposes.
- The page rule applies to that part without opening the rest of the internal tree.
Examples
The panel with part="panel" lives in the shadow root; the ::part(panel) rule lives on the page.
baseline-shadow-card::part(panel)<baseline-shadow-card>
</baseline-shadow-card><div part="panel">
<slot name="content"></slot>
</div>Receives the style: Inner panel with part="panel"
Common mistakes
- Assuming ::part(panel) allows arbitrary internal descendants to be selected.
- Expecting a nested part to cross multiple components without exportparts.
Boundaries and support
The result checks rule acceptance, an independent positive probe, and the witness property on the current subject. It is not a querySelectorAll() count.
exportparts
exportparts forwards a part through another nested component. This scenario has one shadow root and does not need it.
querySelectorAll()
A page query does not cross the shadow root. ::slotted() and ::part() express CSS styling in context; their DOM queries do not provide a comparable count.