Shadow root sandbox
The same hostile email rendered twice, into the light DOM and into a shadow root, to show what each one leaks.
- Published
- Jul 17, 2026
- Origin
- Tatem
- Built with
- Declarative Shadow DOM, Sandboxed iframes, React
Light DOM
Email styles restyle the host toolbar and headers.
Shadow root
Host chrome untouched. Inherited properties still cross the boundary, hence all: initial on :host.
An HTML email arrives with its own stylesheet, written as if it owned the whole document: p turns red and uppercase, .button becomes a black pill, * sets Georgia on everything. Put that straight into a mail client’s page and it doesn’t just style the message. It restyles the client. Both panes above hold the same mock client, and the only difference is where the message lands. Edit the CSS and watch both.
At Tatem, this was the problem underneath every other feature of the mail client. This study is just the boundary. I wrote up the rest in Hosting HTML you didn’t write.
What a shadow root buys
Selectors stop at the boundary in both directions. The email’s .button can’t reach the client’s toolbar, and the client’s p rules can’t reach the email. In the light DOM pane, Reply and Archive have turned into black pills, just because they share a class name with the newsletter’s button. In the shadow root pane they’re untouched. Each side keeps its own class names without negotiating.
What it doesn’t
Inheritance doesn’t stop at the boundary. Color, font size and line height pass from the reading pane into the shadow tree. Uncheck Reset inherited styles and the newsletter picks up the pane’s gray 13px text, which its sender never saw. The fix is :host { all: initial }, declared inside the shadow root. One placement detail matters. A rule the client sets directly on the element the shadow root is attached to wins over :host in the cascade. So I put the reading pane’s typography one level up, where it only arrives by inheritance and the reset can undo it.
A shadow root is a style boundary, not a security boundary. The email’s markup still lives in the client’s page, so scripts, event handlers and remote content need sanitizing either way. Here both panes are sandbox="" iframes, and nothing in them can run.
How it’s built
Each pane is a static srcdoc document in a fully sandboxed frame, so no script can call attachShadow(). The shadow root comes from declarative shadow DOM (<template shadowrootmode="open">) instead. The browser builds it straight from the markup while parsing. Browsers without it (before Chrome 111, Safari 16.4 and Firefox 123) show a one-line fallback in that pane. The email’s CSS is escaped, so typing </style> in the editor can’t break out of its style element. Edits settle for 150 ms before either pane re-renders.
Not yet verified
The fallback in a browser that predates declarative shadow DOM. Safari and Firefox. How VoiceOver, NVDA and JAWS read the two sandboxed panes.