Shadow DOM

Shadow DOM attaches an encapsulated DOM subtree to a host element, helping reusable components keep internal markup and styles from accidental interference. Slots, public APIs, and correctly configured events provide controlled connections to the surrounding page.

Shadow DOM lets an element host a separate DOM subtree, called a shadow tree. The host remains in the regular document tree, while the shadow root is the entry point to the hosted subtree. This structure is commonly used to keep reusable component internals from being accidentally affected by page code.

Attach a root with `element.attachShadow({ mode: "open" })`. The returned `ShadowRoot` can be manipulated much like a document fragment: create nodes, append them, and add styles. An open root is available as `host.shadowRoot`; with a closed root, that property returns `null` to outside code. Closed mode is an access restriction by convention, not a security boundary.

CSS selectors in the document do not normally select elements inside a shadow tree, and selectors inside it do not normally style outside elements. The `:host` selector targets the host from within the shadow stylesheet. CSS custom properties and inherited properties can provide controlled ways to pass styling information across the boundary.

A `<slot>` provides a place where the host's light-DOM children can appear in the rendered component. Slotted nodes remain in the light DOM; slotting changes their rendered placement, not their DOM parentage. This distinction affects queries, event paths, and inheritance behavior.

Shadow DOM does not create a separate JavaScript realm or guarantee secrecy. Components still communicate with their surrounding page through public APIs, attributes, properties, and events. For a custom event intended to cross the shadow boundary, configure `bubbles` and `composed` as needed; `composed: true` allows it to propagate beyond the boundary.

Key points

Interview questions

What is Shadow DOM, and what problem does it solve?

A shadow root is the root node of a DOM subtree attached to a host element. That subtree has its own internal structure and styling context, which limits accidental interference from surrounding page CSS and DOM queries. The host remains in the regular DOM, and the component can expose intended content or behavior through slots and public APIs.

How do open and closed shadow roots differ?

Calling `attachShadow()` on the host creates the root and returns it for populating the component. With `mode: "open"`, outside code can access it through `host.shadowRoot`; with `mode: "closed"`, that property does not expose the root. Closed mode is not a security mechanism because it does not prevent all possible access or inspection. Choose a mode based on the component's API and encapsulation expectations, not to protect secrets.

Can page JavaScript and CSS reach elements inside a shadow tree?

Document-level selectors generally do not search into a shadow tree, and ordinary page styles do not directly match its internal elements. Code with a reference to an open root can query within that root, and styles can be defined inside the shadow tree. The component can still offer deliberate styling hooks, such as CSS custom properties, and use `:host` to style its host from within.

What does a slot do, and does it move a node into the shadow tree?

A slot is a named or default insertion point in the shadow tree for light-DOM children. It affects how those nodes are distributed and rendered, but it does not reparent them: their DOM parent remains in the light tree. This distinction matters when reasoning about selectors, inherited values, and event propagation. A component can use slot-change notifications to respond when assigned content changes.

How do events propagate across a shadow boundary?

Events dispatched inside a shadow tree can be observed by listeners at different points, but the event path is subject to shadow-boundary rules. For a custom event to propagate outward through the boundary, it generally needs `composed: true`; set `bubbles: true` as well when ancestor bubbling is desired. Outside listeners may see a retargeted `event.target`, so inspect `event.composedPath()` when appropriate. Components should document their event names and payloads as part of their public API.