Skip to content

Shadow DOM

Introduction

Shadow DOM is a web platform API that enables encapsulation of DOM trees and CSS styles within web components, preventing style leakage and naming collisions between component internals and the surrounding document. It is a cornerstone of the Web Components standard and fundamentally changes how developers think about building reusable, isolated UI elements. Understanding Shadow DOM is essential for anyone working with modern frontend architectures, design systems, or custom element libraries.

Core Concepts

What Problem Does Shadow DOM Solve?

In traditional web development, every element on a page shares a single, global DOM tree and a single, global CSS scope. This means that a CSS rule like p { color: red; } affects every paragraph on the page, and any JavaScript query like document.querySelectorAll('p') returns every paragraph, regardless of which "component" it conceptually belongs to.

This global nature leads to several critical problems:

  • Style collisions: A class name .button defined in one component can unintentionally override styles in another.
  • DOM pollution: Internal implementation details of a component are visible and accessible to external code.
  • Fragile selectors: Refactoring a component's internal markup can break external CSS or JavaScript that depends on its structure.
  • Lack of true encapsulation: Frameworks simulate encapsulation through conventions (BEM naming, CSS modules, scoped styles), but these are workarounds rather than platform-level guarantees.

Shadow DOM solves all of these by creating a scoped, hidden DOM subtree that is isolated from the main document.

The DOM Tree Model

With Shadow DOM, the browser manages multiple DOM trees for a single page:

  • Light DOM: The regular DOM tree that developers write in their HTML markup. This is what you see when you write standard HTML.
  • Shadow DOM: A hidden DOM tree attached to an element (called the shadow host). Its contents are rendered but are not directly accessible via normal DOM APIs from outside.
  • Flattened Tree: The browser internally composes the light DOM and all shadow DOM trees into a single flattened tree for rendering purposes.

Shadow Root and Shadow Host

The shadow host is any regular DOM element to which a shadow tree is attached. The shadow root is the root node of that shadow tree. It functions like a document fragment but with special encapsulation rules.

Key characteristics of the shadow root:

  • It has its own style scope — styles defined inside do not leak out, and external styles do not penetrate in (with controlled exceptions).
  • It has its own ID namespace — you can have id="title" both inside the shadow root and in the light DOM without conflict.
  • DOM queries from outside (querySelector, getElementById) cannot reach inside the shadow tree.
  • Events that originate inside the shadow tree are retargeted — to outside observers, they appear to come from the shadow host rather than from the internal element.

Open vs. Closed Shadow DOM

When attaching a shadow root, you choose a mode:

Modeelement.shadowRootUse Case
openReturns the shadow rootMost components — allows external tooling and testing to inspect internals
closedReturns nullMaximum encapsulation — used by browser built-in elements like <video>

In practice, open mode is overwhelmingly preferred. Closed mode provides a false sense of security — determined developers can still access internals through other means — and it makes debugging significantly harder.

Style Encapsulation

This is the most impactful feature of Shadow DOM. Styles defined inside a shadow root are scoped to that shadow tree.

How Scoping Works

  1. Styles inside the shadow DOM only apply to elements inside the shadow tree.
  2. Styles outside the shadow DOM do not apply to elements inside the shadow tree (with specific exceptions).
  3. Inherited properties (like color, font-family) do cascade through the shadow boundary because they follow the DOM tree hierarchy.
  4. CSS custom properties (variables) penetrate the shadow boundary, providing an intentional styling API.

Special CSS Selectors for Shadow DOM

SelectorWhere It's UsedPurpose
:hostInside shadow DOMSelects the shadow host element itself
:host(.active)Inside shadow DOMSelects the shadow host only if it has the class .active
:host-context(.dark-theme)Inside shadow DOMSelects the host if any ancestor matches .dark-theme
::slotted(span)Inside shadow DOMStyles light DOM content that has been slotted into a <slot>
::part(label)Outside shadow DOMStyles an element inside the shadow tree that has been explicitly exposed with part="label"

Slots and Content Projection

Slots are the mechanism by which light DOM content is "projected" into a shadow tree. They enable component consumers to inject content into specific locations within a component's internal structure.

There are two types of slots:

  • Named slots (<slot name="header">): Accept only elements with a matching slot="header" attribute.
  • Default slot (<slot>): Catches all light DOM children that don't specify a slot name.

Slots also support fallback content — content placed inside the <slot> tags that renders when no light DOM content is projected into that slot.

Event Retargeting and Composition

When an event originates from an element inside the shadow DOM, the browser retargets it so that listeners outside the shadow boundary see the event as coming from the shadow host.

However, not all events cross the shadow boundary. Events have a composed property:

composed: truecomposed: false
click, focus, blur, input, keydownslotchange, custom events (by default)
Crosses shadow boundaryStays inside shadow tree

To dispatch a custom event that crosses the shadow boundary, you must explicitly set both bubbles: true and composed: true.

Shadow DOM and Accessibility

The accessibility tree (used by screen readers) sees the flattened tree, not the individual shadow and light DOM trees separately. This means:

  • ARIA attributes work across shadow boundaries.
  • Focus navigation traverses into and out of shadow trees naturally.
  • Slotted content retains its accessibility semantics.
  • Labels and aria-labelledby references can be trickier — they may not resolve across shadow boundaries in all browsers.

Shadow DOM in the Web Components Ecosystem

Shadow DOM does not exist in isolation. It is one of three main specifications that together form the Web Components standard:

Declarative Shadow DOM

Traditionally, shadow roots can only be created imperatively via JavaScript. Declarative Shadow DOM is a newer addition that allows shadow roots to be expressed directly in HTML, which is critical for server-side rendering (SSR).

With declarative shadow DOM, a <template shadowrootmode="open"> element inside a host element is automatically promoted to a shadow root by the HTML parser. This means:

  • Shadow DOM components can be rendered on the server without JavaScript.
  • Initial paint is faster because the browser doesn't need to wait for JS to attach shadow roots.
  • Progressive enhancement becomes possible for web components.

Framework Interoperability

Different frameworks interact with Shadow DOM in different ways:

FrameworkShadow DOM SupportNotes
LitFirst-classBuilt entirely around Shadow DOM and custom elements
AngularViewEncapsulation.ShadowDomOffers Shadow DOM as an encapsulation option alongside emulated scoping
ReactLimitedReact's synthetic event system can conflict with Shadow DOM event retargeting; community wrappers exist
VuedefineCustomElement()Can produce Shadow DOM-based custom elements, but most Vue apps use virtual DOM internally
Svelte<svelte:options customElement>Can compile components into Shadow DOM custom elements

Performance Considerations

Shadow DOM has real performance implications:

  • Style recalculation: Browsers can optimize style recalculation because shadow boundaries limit the scope of style invalidation. Changing a class inside a shadow tree doesn't trigger style recalculation for the entire document.
  • Selector matching: Selectors inside a shadow root only need to match against the shadow tree's elements, reducing the search space.
  • Memory: Each shadow root maintains its own style sheet copies, which can increase memory usage when many instances of the same component exist on a page. Adoptable stylesheets (adoptedStyleSheets) mitigate this by allowing multiple shadow roots to share a single CSSStyleSheet object.
  • Layout: Shadow DOM does not create a separate layout context by default — the flattened tree is laid out as one unit.

Best Practices

  1. Prefer open mode over closed: Open shadow DOM enables better debugging, testing, and tooling support while still providing style and DOM encapsulation.
  2. Use CSS custom properties as your styling API: Instead of exposing internal class names, define custom properties (e.g., --button-bg) that consumers can set from outside the shadow boundary.
  3. Expose part attributes sparingly: CSS ::part() pierces the shadow boundary intentionally — treat each exposed part as a public API contract that you must maintain.
  4. Design slots with fallback content: Always provide meaningful default content inside <slot> elements so the component renders sensibly even when consumers provide no projected content.
  5. Use adoptable stylesheets for shared styles: When multiple instances of a component exist, share a single CSSStyleSheet via adoptedStyleSheets to reduce memory consumption.
  6. Set composed: true on custom events that need to escape: By default, custom events do not cross the shadow boundary. Explicitly mark events that external code should observe.
  7. Test accessibility across shadow boundaries: Use the browser's accessibility inspector to verify that the flattened tree correctly exposes labels, roles, and focus order.
  8. Leverage Declarative Shadow DOM for SSR: If your component must render on the server, use <template shadowrootmode="open"> to avoid a flash of unstyled or missing content.
  9. Avoid deep shadow DOM nesting: Components containing components with their own shadow roots create deeply nested trees that are harder to debug and can complicate event handling.
  10. Keep the light DOM API minimal and semantic: The light DOM that consumers write should use meaningful slot names and attributes — the shadow DOM handles all visual complexity internally.