Shadow DOM Encapsulation in Chromium's Blink Engine

This article provides an overview of how the Blink rendering engine in the Chromium browser implements Shadow DOM encapsulation for web components. It covers the core mechanisms Blink uses to enforce boundaries between the main document and isolated components: the TreeScope architecture for DOM isolation, CSS containment via ScopedStyleResolver, the construction of the flat tree for layout, and event retargeting during event propagation.

DOM Isolation via TreeScope

Blink achieves structural encapsulation by separating the main document tree from shadow trees using an internal C++ class called TreeScope. Both the main Document node and the ShadowRoot node implement the TreeScope interface.

Every node in Blink maintains a pointer to its owning TreeScope. Each TreeScope maintains its own isolated lookup tables, including maps for element IDs, named elements, and labels. When a script executes a query method such as document.getElementById() or querySelector(), Blink restricts the search algorithm to the specific TreeScope on which the method was called. Because a ShadowRoot is a separate TreeScope, nodes inside the Shadow DOM are invisible to standard DOM queries originating from the main document.

CSS Encapsulation and ScopedStyleResolver

To prevent CSS rules from leaking in or out of a web component, Blink pairs each TreeScope with its own ScopedStyleResolver.

When a <style> tag or constructed stylesheet is added to a ShadowRoot, Blink parses the CSS rules and stores them exclusivamente within that shadow root's ScopedStyleResolver. During the style recalculation phase, Blink determines the computed style for an element by matching rules only from the ScopedStyleResolver associated with that element's TreeScope.

This architecture enforces two rules:

  • Outer styles do not leak in: Selectors defined in the main document are not evaluated against nodes in a shadow tree. Only inherited CSS properties (such as font-family or color) and CSS custom properties (variables) naturally cross the shadow boundary because they are passed down through computed values rather than selector matching.
  • Inner styles do not leak out: Selectors inside the ShadowRoot are never evaluated against nodes in the main document. Blink only allows a shadow tree to style its own container via specific pseudo-classes like :host and :host-context(), which the engine resolves by temporarily inspecting the shadow host element.

The Flat Tree and Slot Assignment

While the main DOM (Light DOM) and Shadow DOM are logically separated for scripts and styles, Blink must merge them to render the final visual layout on the screen. It achieves this without breaking encapsulation by using a concept called the "Flat Tree."

When a component uses <slot> elements to project Light DOM content into the Shadow DOM, Blink utilizes a SlotAssignment engine. Instead of physically moving the Light DOM nodes into the Shadow DOM—which would alter their parentNode pointers and break encapsulation—Blink constructs a virtual tree structure.

In memory, each DOM node holds pointers for both its actual DOM tree position and its flat tree position. Blink's layout, compositing, and painting engines ignore the standard DOM tree and instead traverse the flat tree. This allows Light DOM nodes to be visually rendered inside the Shadow DOM while logically remaining part of the main document's TreeScope.

Event Retargeting

To maintain encapsulation during user interactions, Blink hides the internal structure of a web component from external event listeners through a process called event retargeting.

When an event (such as a mouse click) occurs on an element inside a Shadow DOM, Blink builds an EventPath by walking up the flat tree from the target node to the root document. This path records the boundaries between different TreeScope instances.

As the event bubbles up and crosses a ShadowRoot boundary into the enclosing TreeScope, Blink modifies the event's properties before invoking any listeners in the outer scope. Specifically, it changes the event.target property to point to the shadow host element rather than the actual internal element that was clicked. To the main document, the event appears to have originated from the custom element itself, keeping the internal shadow nodes completely hidden. If the ShadowRoot was created with mode: 'open', Blink allows developers to bypass this encapsulation only by explicitly calling event.composedPath(), which returns the unedited array of nodes traversed in the flat tree.