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 Rafael Thayto · 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
colorandfont-familypass 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
--ccustom 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-openon 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-indexvalues 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 throughshadowRoot. 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.
- Attributes on the script tag.
data-color,data-position,data-localeanddata-triggercontrol the common choices, and they are read once when the script loads. - The
--ccustom property. The widget sets it inline on the host fromdata-color. Because inline styles beat normal stylesheet rules, a normal rule in your CSS will not override it. Changedata-colorinstead. - The
partattribute. The floating button haspart="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.
Related
- Feedback widget best practicesWhere to place a feedback button, how to keep it fast and accessible, and how to protect it from spam. Practical rules for in-app feedback widgets.
- What should a feedback button say?How to word a feedback button: verbs or nouns, labels that match each page and feedback type, localized copy, and how to replace the default Feedback button.
- Designing a feedback widget for mobile screensDesigning a feedback widget for phones: thumb reach, keyboard overlap, bottom navigation conflicts and the checks to run on a real device before you ship.
- How to make a feedback widget accessibleMake a feedback widget usable with a keyboard and screen reader: real buttons, dialog semantics, focus return, labels, contrast and known gaps.