Use the Shadow DOM¶
The Shadow DOM lets an element host a private subtree – a shadow tree – that composes with the element’s own
children through <slot> elements. turbohtml models the DOM Living Standard tree: Element.attach_shadow(),
ShadowRoot, slot assignment, and the flattened tree. Every algorithm runs in the C core under the per-tree
critical section.
Attach a shadow root¶
Call Element.attach_shadow() with a mode of "open" or "closed". It returns the ShadowRoot, a
document-fragment-like container held off the light tree – it never appears among the host’s children or in its
serialization. Populate it with ShadowRoot.set_inner_html() or ShadowRoot.append():
from turbohtml import Element
host = Element("my-card")
root = host.attach_shadow("open")
root.set_inner_html("<section><slot></slot></section>")
print(root.mode, root.host.tag)
open my-card
An open shadow root is also reachable through Element.shadow_root; a closed one reads None there, so only
the reference attach_shadow returned can reach it. An element can host one shadow root – a second attach_shadow
raises ValueError.
Fill the slots¶
A <slot> in the shadow tree pulls in the host’s direct children. An element is assigned to the slot whose name
matches its slot attribute; the unnamed default slot receives everything else, in order.
Element.assigned_nodes() lists what a slot received, and Element.assigned_elements() drops the text nodes:
from turbohtml import Element, Text
host = Element("my-card")
host.append(Element("h2", {"slot": "title"}, [Text("Hello")]))
host.append(Element("p", None, [Text("body")]))
host.append(Text("loose"))
root = host.attach_shadow("open")
root.set_inner_html('<header><slot name="title"></slot></header><main><slot></slot></main>')
print([node.tag for node in root.select_one('slot[name="title"]').assigned_nodes()])
print([type(node).__name__ for node in root.select_one("main slot").assigned_nodes()])
print([node.tag for node in root.select_one("main slot").assigned_elements()])
['h2']
['Element', 'Text']
['p']
Going the other way, Node.assigned_slot gives the slot a light-DOM child landed in (or None when it is
unassigned or the host’s shadow root is closed):
print(host.children[0].assigned_slot.attr("name"))
print(host.children[1].assigned_slot.tag)
title
slot
The named child lands in the title slot; the plain paragraph lands in the unnamed default slot. An empty slot falls
back to its own children instead, and assigned_nodes(flatten=True) returns that fallback (expanding any nested
shadow slots along the way).
Parse a declarative shadow root¶
A server can ship a shadow tree in the markup itself with a <template shadowrootmode>. When parse() meets one,
it attaches a shadow root to the template’s parent and parses the template’s content into it, exactly as a browser does
on navigation – so the shadow tree is there before any script runs. The template element is consumed: it never joins
the light tree. shadowrootmode is "open" or "closed", and shadowrootdelegatesfocus /
shadowrootclonable set the matching flags:
from turbohtml import parse
doc = parse(
"<article id=post>"
'<template shadowrootmode="open" shadowrootclonable>'
"<h1><slot name=title></slot></h1><slot></slot>"
"</template>"
'<span slot="title">Hello</span><p>World</p>'
"</article>"
)
post = doc.find(id="post")
root = post.shadow_root
print(root.mode, root.clonable)
print(root.html)
print(post.html)
open True
<h1><slot name="title"></slot></h1><slot></slot>
<article id="post"><span slot="title">Hello</span><p>World</p></article>
The shadow root’s slots compose the host’s light children just like a scripted one, so assigned_nodes and
flattened_children work unchanged. A closed root reads None from Element.shadow_root, but its content is
still reachable through Node.flattened_children.
Only a valid shadow host (a flow-content element such as div, section, span, or a custom element with a
hyphenated name) takes a declarative root; on any other parent the template stays an ordinary template. Whole-document
parse() allows declarative shadow roots by default; parse_fragment() does not – pass
allow_declarative_shadow_roots=True to opt in, matching the browser’s setHTMLUnsafe rather than innerHTML.
Read the flattened tree¶
Node.flattened_children returns the composed children a browser would render: a shadow host yields its shadow
tree with each slot replaced by its assigned nodes.
top = Element("my-list")
top.append(Element("li", None, [Text("one")]))
top.append(Element("li", None, [Text("two")]))
top.attach_shadow("open").set_inner_html("<ul><slot></slot></ul>")
ul = top.flattened_children[0]
print(ul.tag, [node.tag for node in ul.flattened_children])
ul ['li', 'li']
Walk flattened_children recursively to render the whole composed subtree.
See also
Shadow DOM for the flattened-tree model and From jsdom for the mapping from
attachShadow and assignedNodes.