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-familyorcolor) 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
ShadowRootare never evaluated against nodes in the main document. Blink only allows a shadow tree to style its own container via specific pseudo-classes like:hostand: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.