Skip to content
AgileLogics
7 min read

Shadow DOM and Slots

Prerequisites: Custom elements

Shadow DOM gives an element a private, encapsulated DOM tree — a shadow root. Styles inside don't leak out, page styles don't leak in (except inherited properties like color and font), and document.querySelector can't see inside.

<style>
  p { color: red; }
</style>

<info-card>
  <span slot="title">Opening hours</span>
  <p>Mon–Sat, 7:00–18:00</p>
</info-card>

<script>
  customElements.define("info-card", class extends HTMLElement {
    constructor() {
      super();
      const root = this.attachShadow({ mode: "open" });
      root.innerHTML = `
        <style>
          :host { display: block; border: 1px solid #ccc; padding: 1rem; }
          h2 { margin: 0; font-size: 1.1rem; }
          p { color: green; }
        </style>
        <h2><slot name="title">Untitled</slot></h2>
        <slot></slot>
        <p>(inside the shadow root)</p>`;
    }
  });

  const card = document.querySelector("info-card");
  const inner = card.shadowRoot.querySelector("p");
  console.log("page p:", getComputedStyle(card.querySelector("p")).color);
  console.log("shadow p:", getComputedStyle(inner).color);
  console.log("document sees shadow h2?", document.querySelector("h2") !== null);
  const [titleSlot, defaultSlot] = card.shadowRoot.querySelectorAll("slot");
  console.log(titleSlot.assignedNodes()[0].textContent);
  console.log(defaultSlot.assignedElements().map(e => e.tagName).join(", "));
</script>
page p: rgb(255, 0, 0)
shadow p: rgb(0, 128, 0)
document sees shadow h2? false
Opening hours
P
  • The page's p { color: red } still styles the light-DOM paragraph, because slotted content stays in the page's DOM and is only displayed in the slot.
  • The shadow root's p { color: green } only affects its own paragraph.
  • <slot name="title"> receives children with slot="title"; the unnamed slot receives everything else. Text inside a slot is the fallback when nothing is assigned.

Styling from outside

  • :host styles the custom element itself from inside; the page can still override it with a normal selector.
  • ::slotted(p) styles slotted elements from inside (only the top-level slotted elements).
  • CSS custom properties pierce the boundary — the standard way to theme components: info-card { --card-accent: navy; }.
  • part="label" on an inner element lets the page style it with info-card::part(label).

open vs closed

mode: "open" exposes element.shadowRoot; "closed" returns null. Closed roots aren't real security — they only make access less convenient — so most components use open.

Events and focus

Events from inside the shadow root are retargeted: listeners on the page see the host element as event.target. Custom events need composed: true to cross the boundary. delegatesFocus: true in attachShadow sends focus to the first focusable element inside.

Accessibility across the boundary

aria-labelledby and for can't reference ids in a different tree (page ↔ shadow root). Keep labels and their controls in the same tree, or use ElementInternals for roles and names.

Related developer tools