0Support

Custom Elements and Shadow DOM selectors

Explore what a rule inside the shadow root selects and what the page can style from outside.

Read the guide

Ask AI

Custom Elements and Shadow DOM selectors

Response generated with openai/gpt-5.4-nano. AI can make mistakes. Always review the result.
Assigned content
Support
Cannot verify in this browser
Observed style
Cannot verify
Style sheet
Inside the shadow root
Subject
Custom element host
Specificity
(0, 2, 0)
HTML
<baseline-shadow-card>
  <span slot="content" class="item">Assigned content</span>
</baseline-shadow-card>
CSS
:host { display: block; }

:host(:state(active)) {
  --baseline-shadow-host-state: applied;
  outline: 3px solid var(--brand-orange);
  background: color-mix(in oklch, var(--brand-orange) 16%, transparent);
}
JS
if (!customElements.get('baseline-shadow-card')) {
  customElements.define('baseline-shadow-card', class extends HTMLElement {
    #internals = this.attachInternals();

    constructor() {
      super();
      const shadow = this.attachShadow({ mode: 'open' });
      const style = document.createElement('style');
      style.textContent = ":host { display: block; }\n\n:host(:state(active)) {\n  --baseline-shadow-host-state: applied;\n  outline: 3px solid var(--brand-orange);\n  background: color-mix(in oklch, var(--brand-orange) 16%, transparent);\n}";
      shadow.append(style);
      const panel = document.createElement('div');
      panel.setAttribute('part', 'panel');
      panel.textContent = "Inner panel";
      const slot = document.createElement('slot');
      slot.name = 'content';
      panel.append(slot);
      shadow.append(panel);
    }

    setActive(active) {
      if (active) this.#internals.states.add('active');
      else this.#internals.states.delete('active');
    }
  });
}

document.querySelectorAll('baseline-shadow-card').forEach((card) => {
  card.setActive(true);
});

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.

Back to the playground

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

:host

Selects the host from a stylesheet inside its shadow root.

Specificity(0, 1, 0)
Behavior
  • 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.

CSS selector · Inside the shadow rootSpecificity(0, 1, 0)
:host
Page HTML
<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.
Read the full definitionOpens in a new tab

Host class

:host(.selected)

Selects the host when it has the selected class.

Specificity(0, 2, 0)
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.

CSS selector · Inside the shadow rootSpecificity(0, 2, 0)
:host(.selected)
Page HTML
<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).
Read the full definitionOpens in a new tab

Host custom state

:host(:state(active))

Selects the host with the active custom state from its internal sheet.

Specificity(0, 2, 0)
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.

CSS selector · Inside the shadow rootSpecificity(0, 2, 0)
:host(:state(active))
Page HTML
<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.

Read the full definitionOpens in a new tab

State from the page

baseline-shadow-card:state(active)

Selects the custom element with the active state from the page.

Specificity(0, 1, 1)
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.

CSS selector · On the pageSpecificity(0, 1, 1)
baseline-shadow-card:state(active)
Page HTML
<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.

Read the full definitionOpens in a new tab

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)
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.

CSS selector · Inside the shadow rootSpecificity(0, 2, 0)
:host-context(.theme-active)
Page HTML
<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.

Read the full definitionOpens in a new tab

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)
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.

CSS selector · Inside the shadow rootSpecificity(0, 2, 2)
slot[name="content"]::slotted(.item)
Page HTML
<baseline-shadow-card>
  <span slot="content" class="item">Assigned content</span>
</baseline-shadow-card>
HTML inside the shadow root
<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.
Read the full definitionOpens in a new tab

Slot with content

slot[name="content"]:has-slotted

Selects the content slot when it has assigned nodes.

Specificity(0, 2, 1)
Behavior
  • 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.

CSS selector · Inside the shadow rootSpecificity(0, 2, 1)
slot[name="content"]:has-slotted
Page HTML
<baseline-shadow-card>
  <span slot="content" class="item">Assigned content</span>
</baseline-shadow-card>
HTML inside the shadow root
<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.

Read the full definitionOpens in a new tab

Exposed part

baseline-shadow-card::part(panel)

Styles the internal panel exposed as part="panel" from the page.

Specificity(0, 0, 2)
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.

CSS selector · On the pageSpecificity(0, 0, 2)
baseline-shadow-card::part(panel)
Page HTML
<baseline-shadow-card>
</baseline-shadow-card>
HTML inside the shadow root
<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.
Read the full definitionOpens in a new tab

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.