Shadow roots
A shadow root is a second, private DOM tree attached to an element. Selectors written for the page stop at its edge, and selectors written inside it never get out.
The heading above is a shadow host. Its dashed frame and label are drawn by a stylesheet inside its shadow root, and the words "Shadow roots" are ordinary page text shown through a slot. Throughout this page, solid frames mark the page's own DOM and dashed frames mark a shadow tree.
Attaching one takes a single call
The element you call attachShadow() on becomes the shadow host. Whatever you put in the returned root is rendered in place of the host's own children.
const host = document.querySelector('#host');
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = `
<style>p { color: rebeccapurple; }</style>
<p>Hello from the shadow tree</p>
`;
That p rule looks dangerously broad, but it only applies inside this root. The first example shows why.
Styles stop at the boundary
Both stylesheets below are live. Write the broadest selector you can think of in either one and watch where it lands.
Paragraph in the page
The markup on both sides is identical: one <p> and one <button>. Only the tree they live in differs.
Inherited values and custom properties pass through
The boundary blocks selectors, not inheritance. A shadow tree's content is still a descendant of its host, so color, font and every custom property flow in. That makes custom properties the standard way to theme a component from outside.
With all: initial the component stops inheriting colour and font, but --accent still gets in. The all shorthand does not touch custom properties.
Paragraph in the page
Slots show the host's children inside the shadow tree
Once a host has a shadow root, its own children are no longer rendered unless the shadow tree contains a <slot>. Slotted nodes are displayed at the slot's position but never move: they stay in the page DOM, so page CSS still styles them. From inside, ::slotted() can style them too, and :host styles the host element itself.
Remove the children to see each slot's fallback content. Delete the unnamed <slot> and the paragraph disappears from view while remaining in the DOM.
Parts let the page style chosen elements
A component can opt specific elements into outside styling by giving them a part attribute. The page then reaches them with ::part(). Anything without a part name stays private.
Inside the shadow root of <fave-button>
<button part="button">
<span part="icon">★</span>
<span part="label">Favourite</span>
</button>
<p class="hint">I have no part attribute.</p>
What JavaScript can see
DOM queries stop at the boundary just as selectors do. With mode: 'open' you can step inside through host.shadowRoot. With 'closed' that property is null, and only code that kept the return value of attachShadow() can get in. Events that leave a shadow tree are retargeted so outside listeners see the host as the target.
Run from the page
Output
Click either button. A listener on the page logs what it receives.
Shadow roots without JavaScript
A <template shadowrootmode="open"> placed directly inside an element tells the HTML parser to attach a shadow root as it reads the page. This is declarative shadow DOM, and it lets servers send components that render before any script runs.
It only works where the parser is told to allow it: the initial page load, or setHTMLUnsafe(). Plain innerHTML leaves the template inert. Switch between the two below.
What crosses the boundary
| Mechanism | Crosses? | Detail |
|---|---|---|
Page selectors such as button or * | No | They never match elements in a shadow tree. |
| Selectors inside the shadow root | No | They never match elements in the page. |
| Inherited properties | Yes | Block them with :host { all: initial; } if needed. |
| Custom properties | Yes | The usual theming hook. Not reset by all. |
::part(name) | Opt-in | Only for elements the component marks with part. |
::slotted(sel) | Inward only | Styles top-level slotted nodes, not their descendants. Page rules win ties. |
document.querySelector() | No | Go through host.shadowRoot when the mode is open. |
| Events | Most | Composed events bubble out with target retargeted to the host. |