What is the Shadow DOM — and why your CSS can't reach the widget
You dropped a ready-made chat widget onto your site. Its buttons are too round, so you write button { border-radius: 4px } in your CSS. Nothing changes. You add !important. Still nothing.
Meanwhile the widget's text has somehow picked up your font and your color. And document.querySelector(".send") returns null, even though you're looking at the "Send" button right now.
These aren't three different bugs. It's one thing — the Shadow DOM.
What the Shadow DOM is, in plain words
A normal page is one big tree of elements — the DOM. Every CSS selector you write and every querySelector search that whole tree.
The Shadow DOM is a separate little tree attached to a single element. Like a glass dome over a flower bed in the middle of a garden. You can see the bed; it's part of the garden. But a gardener outside with shears can't reach it.
The element the dome is attached to is called the host. The hidden tree itself is the shadow root. In DevTools you'll see it as a #shadow-root (open) line inside the element.
Why bother? So the widget works the same on any site. The chat's author has no idea what your styles are. If your button rule broke their buttons, they'd have to patch the widget for every customer. The dome protects both ways: its styles don't leak out and wreck your page either.
What passes through the dome and what doesn't
This is where the font mystery hides.
Selectors don't pass. A page rule like button { } simply doesn't see buttons under the dome. Specificity and !important don't matter: the selector never matches the element at all. It's a common reason your CSS isn't working while the inspector shows the rule wasn't even applied.
Inherited properties pass. color, font-family, font-size, line-height inherit from parent to child. And the host is the parent of everything under the dome. So your page's font and color leak inside, but border-radius doesn't: it isn't inherited.
CSS variables pass. They inherit too. And that's the official way to configure a widget from outside. A good author writes background: var(--chat-accent, blue) inside, and you set --chat-accent: #d4a373 on your side. More in the article on CSS variables.
Marked parts pass. The author can tag an element with part="send". Then you can style it from outside like this: chat-widget::part(send) { border-radius: 4px }. Only the parts they've allowed.
Why querySelector can't find the button
document.querySelector also searches only the main tree. It doesn't look under domes.
If the dome is open (open), you can step in explicitly:
const widget = document.querySelector("chat-widget");
const send = widget.shadowRoot.querySelector(".send");
If it's closed (closed), widget.shadowRoot returns null. The author deliberately locked the inside — and you shouldn't work around it: your code will break when the widget updates.
Events tell a similar story. A click on a button inside bubbles out, but your event.target shows the host, not the button. The dome swaps the target so it doesn't reveal its insides. The real click path comes from event.composedPath() — for an open dome.
Where you've already met the Shadow DOM
Every day, you just didn't know it.
- Built-in browser elements. The player buttons of
<video controls>, the<input type="range">slider, the date picker — all are the browser's own shadow trees. That's why a slider is so hard to recolor: you're only let in via special pseudo-elements like::-webkit-slider-thumb. - Web components. Custom tags like
<chat-widget>or<model-viewer>almost always live under a dome. - Embeds. Payment, support and review widgets. The dome is exactly what keeps them from breaking on someone else's site.
To see the browser's domes in DevTools, turn on "Show user agent shadow DOM" in settings.
How to ask AI to restyle a widget
Out of habit, a neural net will suggest !important and heavier selectors. Explain the mechanism:
styles don't apply to the widget buttons, fix itThe chat-widget renders its buttons in a Shadow DOM, so my CSS button selector never reaches them. Do not suggest !important or stronger selectors. Check which CSS variables and which part attributes the widget exposes, and set the button corner radius through them. If it exposes neither, say so.Are the Shadow DOM and the Virtual DOM the same thing?
No. The Virtual DOM is React's way of updating the page quickly; it lives in memory. The Shadow DOM is real isolation of a piece of the page in the browser. They only share the word DOM.
Can you create a Shadow DOM without JavaScript?
Yes. A <template shadowrootmode="open"> tag inside an element creates the dome straight from HTML. All major browsers have supported it since 2024, including for server-side rendering.
How do I reset the inherited font inside my own component?
Inside the dome, style the host: :host { font-family: system-ui; color: inherit } or specific values. A rule on :host overrides whatever came in through inheritance.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.