Why embeddable widgets use Shadow DOM

Shadow DOM keeps a widget's styles and markup separate from the page, so site CSS does not break the widget and the widget does not restyle the site. It does not isolate global JavaScript, document events or inherited properties, so a widget still has to reset and scope those.

By · Last updated

Why a widget cannot trust the host page's CSS

A feedback widget runs on someone else's website. That site may load a CSS reset, a framework stylesheet, a design system or a set of global rules such as button { background: red } that were written for a different part of the app. Any of them can change the look of a widget that shares the same page. The widget's own styles can cause the same damage in reverse, by restyling buttons, inputs and headings the site owner built on purpose.

Shadow DOM is the browser feature that solves this. It gives a component its own DOM subtree with its own styles, so the rules on either side stop at the boundary.

What Shadow DOM isolates

When the Escuta Produto script runs, it creates a host element with the attribute data-escuta-produto and attaches an open shadow root to it. Every visible part of the widget lives inside that root: the floating button, the panel, the form, the thank you screen and the styles that go with them.

Two things follow. A site rule such as button { border-radius: 0 } does not reach the widget's buttons, because the selector does not match elements inside another shadow tree. And the widget's own rules, such as its font stack and its input borders, do not change the rest of the page. Both directions are isolated.

What Shadow DOM does not isolate

Shadow DOM is a boundary for the DOM and for most styles. It is not a sandbox, and several things cross it:

  • Inherited properties. Properties such as color and font-family pass from the host into the shadow tree unless something resets them. The widget sets :host{all:initial} on its host and sets its own font family on every element inside.
  • Custom properties. They always cross the boundary. The widget reads its accent color from a --c custom property that it sets on the host.
  • Global JavaScript. The widget exposes window.EscutaProduto, and any script on the page can call it. Shadow DOM does nothing to protect it.
  • Document events. The widget listens for Escape and for clicks on elements with data-escuta-open on the document. Events from inside the shadow tree bubble up, so page scripts can observe them.
  • Stacking. A shadow root does not lift its contents above the rest of the page. The widget uses very high z-index values on its fixed elements so the panel stays on top of your page's own layers.
  • Access to the root. A root opened with mode: "open" can be read by page code through shadowRoot. Open mode is what testing tools need. It is not a security boundary, so never put a secret in a widget and assume the shadow root hides it.

These are not bugs in Shadow DOM. They are the boundaries of what it does, and a good widget reset and scope exactly these properties.

Theming across the boundary

A widget that wants to be themed from outside should expose a small, documented surface. The Escuta Produto widget has three.

  1. Attributes on the script tag. data-color, data-position, data-locale and data-trigger control the common choices, and they are read once when the script loads.
  2. The --c custom property. The widget sets it inline on the host from data-color. Because inline styles beat normal stylesheet rules, a normal rule in your CSS will not override it. Change data-color instead.
  3. The part attribute. The floating button has part="button", so you can style it from your own stylesheet with ::part():
[data-escuta-produto]::part(button) {
  border-radius: 8px;
  letter-spacing: 0.02em;
}

Rules from the outer page that target ::part win over the widget's own rules for normal declarations, which is why this works. Only the floating button is exposed. The panel, the form and the stars are not, so you cannot restyle them this way. If you need a different panel, build your own form on the REST API.

Testing and automation through shadow roots

Plain DOM queries do not look inside a shadow root. document.querySelector("button") will never return a widget button, and a snippet such as document.querySelector(".fab") returns null. To reach the floating button in a script you have to go through the host: document.querySelector("[data-escuta-produto]").shadowRoot, then query inside it.

Some browser test tools, Playwright among them, pierce open shadow roots when you use their own locators, so a test that finds the feedback button by its visible text can work without extra code. Other tools need explicit steps. Check your tool's documentation before you write selectors by hand.

A second practical point: the widget mounts once, on the body. If your app replaces the contents of the body during navigation, the host element can disappear. The widget does not watch for that and mount itself again. Check that the button is still there after a route change, and load the script again if it is not.

Where the Escuta Produto widget draws the line

The widget uses Shadow DOM for what it should isolate: its markup, its styles and its fonts. It leaves the global window.EscutaProduto object, the document events and the custom property boundary open, because the public API needs them. You get a small set of attributes, the part hook on the button and a JavaScript API with open, close, identify and setMetadata.

For setup steps and every attribute, see the widget docs. For the security side of loading a third-party script, read content security policy for third-party widgets. For how the panel behaves with a keyboard, see how to make a feedback widget accessible.

How to check the boundary with the Escuta Produto widget

Load the page and run three checks. First, change a global button style in your own CSS and confirm the widget button keeps its look. Second, call EscutaProduto.close() from your console and confirm the panel closes, which shows the global API is reachable. Third, navigate between two routes in your app and confirm the floating button is still on the page. If the second check works and the third one does not, you know exactly where the boundary is on your site.

Frequently asked questions

Why do embeddable widgets use Shadow DOM?

Shadow DOM gives the widget its own markup and styles, so the host page's CSS cannot break it and the widget cannot restyle the page. It is the standard way to keep a third-party component visually separate.

Does Shadow DOM stop a widget from affecting my JavaScript?

No. Shadow DOM isolates markup and most styles, but global objects, document-level events and custom properties still cross the boundary. A well-built widget exposes a small public API and nothing more.

How can I style a widget that uses Shadow DOM?

Use the attributes the widget documents, such as its accent color and position, and use the part selector for elements that expose one. In the Escuta Produto widget, the floating button has a part, so you can restyle it from your own stylesheet.