Embedding a chat UI in an arbitrary web page is a small problem that grows fast. Day one: it works on your demo page. Day two: someone embeds it on a site with * { box-sizing: border-box } somewhere weird and your padding goes haywire. Day three: a customer’s host CSS has body { font-family: Comic Sans } and now your chat is Comic Sans.

You can fight this with !important everywhere, which is awful. You can fight it with CSS specificity hacks, which is fragile. Or you can stop fighting and use the Shadow DOM.

Linda’s chat UI lives in a Shadow DOM. Here’s why, what it gets us for free, and the surprising number of issues this design solves before they show up.

The Shadow DOM primer

Shadow DOM is a browser-native style isolation primitive. You attach a “shadow root” to a host element, render your subtree inside it, and the page’s CSS can’t reach in (mostly). Your CSS, conversely, can’t reach out.

<div id="linda-host"></div>
<script>
  const host = document.getElementById("linda-host");
  const shadow = host.attachShadow({ mode: "open" });
  shadow.innerHTML = `
    <style>
      .chat { background: white; padding: 16px; }
    </style>
    <div class="chat">Hello.</div>
  `;
</script>

Now your .chat class is scoped to the shadow root. No matter what the host page’s CSS does, the .chat inside your shadow is isolated.

What this solves

Real things we’ve seen happen when you don’t use Shadow DOM:

  • Reset stylesheets clobbering your padding. Host pages with * { margin: 0; padding: 0 } ruin spacing.
  • Font inheritance. Hosts with body { font-family: Comic Sans } propagate to your chat.
  • Specificity wars. Customer ships .chat-bubble { background: red !important } to “match brand” and breaks your dark-mode logic.
  • box-sizing mismatches. Host uses content-box somewhere; your buttons end up the wrong size.
  • CSS variable collisions. Host defines --primary for their brand; you define --primary for your accent. Whose wins depends on tree position. Chaos.

Each of these is fixable with !important and specificity tricks. They all go away with Shadow DOM.

What Shadow DOM doesn’t solve

It’s not a perfect wall. Things that still cross:

  • Inherited CSS properties. font, color, direction, etc. — if your shadow root doesn’t set them explicitly, they inherit from the host. (Linda sets all the relevant ones on the shadow root.)
  • CSS custom properties. Variables defined on :root do cross into the Shadow DOM. This is actually useful — you can let host pages theme your chat by defining --linda-accent.
  • Form elements. <input> and <button> have user-agent styles that bleed in. Linda explicitly resets these inside the shadow.

The leakage is small and surgical. With a careful reset (~30 lines of CSS at the top of the shadow), it’s bulletproof.

The Linda implementation

Linda’s chat is mounted into a shadow root on a <linda-chat> custom element. Inside the shadow:

class LindaChat extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: "open" });

    // Inject a reset + the chat CSS.
    const styles = document.createElement("style");
    styles.textContent = LINDA_RESET + LINDA_CHAT_CSS;
    shadow.appendChild(styles);

    // Mount the chat subtree.
    const root = document.createElement("div");
    root.className = "linda-root";
    shadow.appendChild(root);

    // ...attach the actual chat component.
  }
}
customElements.define("linda-chat", LindaChat);

The reset:

.linda-root, .linda-root * {
  box-sizing: border-box;
  font-family: var(--linda-font, -apple-system, BlinkMacSystemFont, sans-serif);
  color: var(--linda-ink, #0b1020);
  font-size: 16px;
  line-height: 1.5;
  margin: 0;
  padding: 0;
}

That’s the magic. Everything inside .linda-root starts from a known state. Host CSS can’t intrude.

Theming through custom properties

Custom properties cross into the shadow. So we expose a documented set:

:root {
  --linda-accent: #4f46e5;      /* your brand */
  --linda-surface: #ffffff;     /* chat bg */
  --linda-ink: #0b1020;         /* text */
  --linda-radius: 12px;         /* corners */
  --linda-font: system-ui;      /* font */
}

Host pages set whichever they want. The chat picks them up. Themed chat in 3 lines of host CSS.

Accessibility surprises

The big worry about Shadow DOM is “does it break screen readers?” The answer: not if you do it right.

  • ARIA roles cross the shadow. A role="dialog" inside the shadow announces correctly.
  • Focus management works. aria-activedescendant, focus traps, all functional.
  • Live regions announce. aria-live="polite" inside the shadow works for streaming chat messages.

What you have to watch for:

  • <label for=...> doesn’t cross the shadow. Use aria-labelledby or wrap labels.
  • The host page’s “skip to content” link can’t target your shadow. Provide your own skip link inside the shadow.

Linda’s chat ships with a skip link, focus management, ARIA live regions for streaming responses, and keyboard navigation (Tab through buttons, Enter to send, Escape to close).

When you’d skip the Shadow DOM

A few cases where Shadow DOM is wrong:

  • You want the chat to inherit ambient styles. If your chat is supposed to look exactly like the host site (matching fonts, colors, spacing), inheriting is what you want.
  • You’re shipping into a known environment. If the chat only lives inside your own site, where you control all the CSS, Shadow DOM is overhead.
  • Heavy DOM interaction with the host page. Shadow DOM makes “find the button in the host” slightly more verbose.

For Linda’s case — chat lives on arbitrary customer pages, must not break — Shadow DOM is the clear win.

The principle

Defensive defaults beat clever escape hatches.

Most “embed this on your page” problems come from designs that assume best-case host CSS. The script tag works on the demo and breaks in production. Shadow DOM flips the default: you start from a known state, and host customization is opt-in via documented custom properties.

This is the same shape as Linda’s broader bet — agents shouldn’t assume their context is friendly. The VFS lets the model navigate explicitly. The Shadow DOM lets the chat render predictably. Both come from the same instinct: be defensive about the substrate, then build clean things on top.

What you can take from this

If you’re embedding a UI on third-party sites for any reason — a widget, a chat, a notification toast, a CMS overlay — use a Shadow DOM. The 30 lines of reset CSS are worth more than every CSS-specificity hack you’d otherwise write. The cost is real (slightly weirder dev experience, some accessibility nuance) and the wins compound over time.

For Linda’s specific implementation, see /features/vfs (the related architectural choice on the agent side) and the chat code in packages/core. It’s the smallest file in the whole runtime, and it’s the one that prevents the most production incidents.

FAQ

Does the Shadow DOM hurt accessibility?

No — properly configured, Shadow DOM is transparent to assistive tech. The chat respects ARIA landmarks, focus management, and keyboard navigation.

Can I customize the chat appearance?

Yes. CSS custom properties are exposed for the major surfaces (background, accent, text). Or use the headless mode and render your own chat UI.