In the DOM, what does calling element.attachShadow({ mode: 'open' }) do, and what does the resulting shadow boundary actually encapsulate?
answer
- a second tree hanging off the host
- selectors stop at the boundary, both ways
- ids are scoped per root
- only some elements may host one
basics
~20 sattachShadow creates a ShadowRoot: a separate DOM subtree rendered in place of the element's own children. Its boundary scopes selectors both ways, so outer CSS and document.querySelector cannot reach inside, and styles defined inside cannot leak out.
solid answer
~50 s`attachShadow` attaches a `ShadowRoot` to the element and returns it; in open mode the element also exposes it as `el.shadowRoot`, and the root points back through `shadowRoot.host`. From then on the browser renders the shadow tree in place of the element's own children, and the boundary scopes name lookups in both directions: a selector written in the document — including `document.querySelector` — does not match nodes inside the root, and a `<style>` inside the root cannot match anything outside it. IDs and classes are scoped to that tree, so many instances can reuse the same id safely. Plenty still crosses: the host is an ordinary element the page styles and lays out, composed events propagate out (with their target retargeted), and an open root is reachable from any same-page script. `attachShadow` is only allowed on valid custom-element names and a fixed list of plain elements such as `div`, `span`, `p` and `section`.
code
javascript · 10 linesconst host = document.createElement('div');
document.body.append(host);
const root = host.attachShadow({ mode: 'open' });
root.innerHTML = '<style>p { color: crimson }</style><p id="label">inside</p>';
console.log(document.querySelector('#label')); // null - query stops at the boundary
console.log(root.querySelector('#label').id); // "label"
console.log(root.host === host); // true
console.log(host.shadowRoot === root); // true (open mode)go deeper
Be ready to say plainly that attachShadow makes a second, scoped DOM tree, and that a document-level querySelector or CSS rule will not reach into it. Knowing you must go through el.shadowRoot is most of the answer.
Explain the mechanics: the root is a DocumentFragment linked by host, the shadow tree renders in place of the light children, and ids and selectors are scoped per root while the host itself stays an ordinary element in the page.
Show where encapsulation stops. Be able to list what still crosses — events, layout, focus order, script access through an open root — and explain how that shapes debugging when a component looks broken only in one host page.
Own the tradeoff. Argue when platform-level scoping is worth the cost of routing every query, global stylesheet and test selector across the boundary, versus build-time scoping that keeps one flat tree.
## What the call creates `Element.attachShadow(init)` creates a `ShadowRoot` — a special `DocumentFragment` — and attaches it to the element, which is then called the *shadow host*. The call returns the root, and `mode` is a required member of the init dictionary: ```js class MyCard extends HTMLElement { constructor() { super(); const root = this.attachShadow({ mode: 'open' }); root.innerHTML = `<style>p { color: crimson }</style><p id="label">Hello</p>`; } } customElements.define('my-card', MyCard); ``` The root is not a child node in the normal sense: `host.childNodes` does not contain it, and `shadowRoot.parentNode` is `null`. The link back is `shadowRoot.host`, and with `mode: 'open'` there is also a forward link, `host.shadowRoot`. Two trees now exist for one element. The nodes the page author wrote between the tags are the *light DOM*; the nodes inside the root are the *shadow DOM*. Once a root is attached, the browser renders the shadow tree in the host's place — the light children are still in the DOM and still scriptable, but they are only painted where the shadow tree explicitly makes room for them. ## Where attachShadow is allowed Not every element accepts a shadow root. The host must either have a valid custom element name (a dash in it, like `my-card`) or be one of a fixed list of ordinary elements: `article`, `aside`, `blockquote`, `body`, `div`, `footer`, `h1`–`h6`, `header`, `main`, `nav`, `p`, `section`, `span`. Calling it on `<input>`, `<button>`, `<img>` or `<textarea>` throws a `NotSupportedError`, because those elements already have browser-internal shadow trees or a rendering model that cannot host one. Calling it twice on the same element throws as well. ## What the boundary blocks The boundary is a **scoping** boundary for names and selectors, and it works in both directions. *Selectors do not cross inward.* A document stylesheet rule such as `.card p { color: blue }` never matches a `<p>` inside a shadow root, no matter how specific it is. That is why the component's own `<style>` block is reliable: nothing in the host page can accidentally restyle its internals. *Selectors do not cross outward.* The `p { color: crimson }` above styles only paragraphs inside that one root. Two components can both ship a bare `p` rule without colliding. *DOM queries do not cross inward.* `document.querySelector('#label')` returns `null` for the example above; you must go through the root: `document.querySelector('my-card').shadowRoot.querySelector('#label')`. The same applies to `document.getElementById`, and to `Element.closest()` called from inside the shadow tree — it walks up to the shadow root and stops, because the root is not an element. *IDs are scoped.* Every shadow root has its own id namespace, so a hundred instances of a component can each contain `#label` without conflict — the classic reason component authors reach for shadow DOM at all. ## What still crosses Encapsulation is not isolation, and interviewers probe exactly here. - **The host is a normal element.** The page selects it by tag or class, gives it margins, and puts it in the layout and focus order like anything else. Note that a custom element is `display: inline` by default until something says otherwise. - **Events escape.** Most UI events are dispatched with `composed: true`, so a click inside the shadow tree still bubbles out to `document` — with `event.target` rewritten to the host so the outside never learns the internal structure. - **Script still reaches in.** Open mode is a convention, not a wall: any script on the page can read `el.shadowRoot` and rewrite the internals. - **The tree is real DOM.** Shadow nodes participate in rendering, hit-testing and the accessibility tree; they are not a rendering trick. ## Why it exists Before shadow DOM, component styles were guarded by naming conventions — BEM prefixes, hashed class names from a bundler — and internal structure was guarded by nothing at all. Shadow DOM moves both guarantees into the platform: the browser, not a build step, is what stops the page's `p { margin: 0 }` from reaching your widget's internals. The price is that everything which used to be trivially global — a document-wide query, a design-system stylesheet, a `querySelector` in an end-to-end test — now has to be explicitly routed across the boundary.
- What happens to the element's existing children once you attach a shadow root to it?They stay in the DOM as the element's light DOM — still in `host.childNodes`, still scriptable — but they stop being rendered on their own. The shadow tree is rendered in their place, and the light children only appear where the shadow tree provides a place for them. Nothing is deleted; the rendering just no longer follows `childNodes`.
- Why does attachShadow throw on an <input> element?`attachShadow` is restricted to valid custom-element names and a fixed list of plain elements (`div`, `span`, `p`, `section`, `header` and a handful more). Elements like `input`, `button`, `textarea` and `img` already carry browser-internal shadow trees or have a rendering model that cannot host one, so the call raises a `NotSupportedError`.
- Does the shadow boundary hide the component from assistive technology?No. Shadow nodes are real DOM and are exposed in the accessibility tree normally — a heading or a button inside a shadow root is announced like any other. What the boundary scopes is CSS selector matching and DOM lookups, not semantics or rendering.
A shadow root is like a room with its own light switch: the page can move the room around and knock on the door, but the wiring inside is on a separate circuit.
saying these in an interview costs you the question
- Thinks shadow DOM hides markup from view-source or devtools
- Says document.querySelector finds nodes inside an open root
- Believes attaching a shadow root deletes the light children
- Claims shadow DOM is only a styling trick with no real DOM
- Tries to attachShadow on an input or button and expects it to work