Why our widget lives inside a Shadow DOM
Embedding third-party UI on someone else's page is a CSS minefield. Shadow DOM is the only sane defuser.
If you have ever shipped an embeddable widget, you know the moment: the support engineer pings you with a screenshot of your perfectly designed chat bubble, now squashed, recolored, and missing its border because the host site set * { box-sizing: content-box } and button { font-size: 22px !important } thirteen years ago.
You cannot win a CSS war on someone else’s repo. You can only secede.
What Shadow DOM gives you
A Shadow root is a parallel DOM tree that the host’s stylesheets cannot reach. From the host’s perspective the widget is a single opaque element. From inside the widget, host CSS does not exist.
That isolation buys three things:
- Predictable rendering across every CMS, marketing template, and Wordpress theme on earth.
- No font-size cascades — your
12pxstays12px. - No specificity arms races — you do not need
.fb-button.fb-button--primary { font: 14px/1.4 system-ui !important }written six layers deep.
What it costs
- Slot composition is awkward. Passing styled children in is harder than a normal portal.
- Devtools are clunkier. Inspecting a shadow tree is a couple extra clicks.
- Some libraries assume document scope. Tooltip libraries that append to
document.bodyneed to be told otherwise.
We decided the tradeoffs were obvious. The widget should look the same on a Webflow landing page and a hand-rolled Rails app. Shadow DOM is the only mechanism that delivers that without asking customers to “please remove this rule from your global stylesheet.”
A small lesson about themes
Theme tokens still flow in via CSS custom properties — those do pierce the shadow boundary. So the visitor’s host page can override --fb-primary-color and the widget reskins instantly. Boundary, but with a controlled door.