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></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 withslot="title"; the unnamed slot receives everything else. Text inside a slot is the fallback when nothing is assigned.
Styling from outside
:hoststyles 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 withinfo-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.