Scoping widget CSS without Shadow DOM: how we do it and where it falls short
Widget Storm widgets render straight into your page with no Shadow DOM. Here is how their CSS stays contained, what that looks like in the browser, and where it falls short.
During our DevHunt launch someone asked a good question. If widgets render straight into the host page, how do you stop their CSS from spilling onto everything else? Shadow DOM, or something lighter? And what happens when it goes wrong?
We don't use Shadow DOM. There is no attachShadow and no custom element anywhere in Widget Storm. This post shows what we do instead, what it looks like in the browser, and where it falls short. If you are still choosing between an iframe and a native embed, we compared the two in Native widgets vs. iframe embeds.
Why not Shadow DOM
Shadow DOM gives you a clean boundary. Styles inside stay in, styles outside stay out. For a lot of components that's exactly what you want.
For our widgets it's the wrong trade. A widget is supposed to look like part of your page, and behind a shadow root your stylesheet can't reach it. We would have to expose every adjustable spot through custom properties or ::part. Server-side rendering also gets more involved, because the shadow root has to be declared in the HTML.
So our widgets live in your DOM like any other markup, and your CSS applies to them.
How a widget's CSS stays contained
Two conventions do the work. Neither of them is a sandbox.
The first is a placeholder. A widget author writes ##self##, and before delivery we replace it with the widget's name plus a hash of the instance. This rule
.##self## .title { font-weight: 600; }arrives in the browser looking something like this:
.wg_countdownb3e2705351409f2245a80616cb683a45 .title { font-weight: 600; }That name is real. It's what our countdown is called when you embed it with the script tag and no parameters. Embed the same thing a second time on the same page and the second copy becomes …cb683a45_2, in its CSS, HTML and JavaScript alike. Two identical widgets don't step on each other.
The second convention is a fixed prefix. Our support chat puts wg-ch in front of every rule, so nothing in it targets a plain button or p on your page.
JavaScript follows the same idea. Each of our widgets only looks for elements carrying its own name, and the chat only for its own wg-ch containers. None of them touches a generic class on your page.
Where it falls short
An author can still write a global rule. If a widget's CSS says button { … }, every button on your page gets it, and we can't prevent that. Scoping only holds when every selector carries the placeholder or a prefix. Ours do. For a widget by someone else, check before you embed it.
On the server, copies share a name unless you give them one. Without your own id, every copy of a widget rendered server-side gets the same name, even with different parameters. If they need to look different, pass each one its own id as the fifth argument of $wg->get(). That works from handler version 4.3 on; the version is at the top of wghandler.php. If yours is older, download it again from the Station.
Your CSS reaches into the widget, and that's on purpose. A broad rule like div { padding: 2rem } lands there too. That's why our chat sets box-sizing, font sizes, line height and margins itself instead of inheriting them. Still, no widget can defend against every rule.
If you write widgets yourself
These four habits keep a widget from leaking into other people's pages:
- Every selector starts with
.##self##or your own prefix. No plainbutton,por*. - JavaScript only searches inside its own container, for example with
document.querySelectorAll('[data-wg-self="##self##"]'), never withdocument.querySelector('.title'). - Whatever the layout depends on, the widget sets itself:
box-sizing, font size, line height. - If you render a widget more than once on the server, give each copy its own id as the fifth argument.
What you get in return
The widget picks up your fonts. You restyle it from your own stylesheet without learning an API. Rendered on the server, it's in the first HTML and doesn't wait for JavaScript. And there's no iframe whose height, scrolling and state you have to keep in sync.
For code you don't trust, Shadow DOM or an iframe is the right call. For widgets that should feel like part of your site, we think a strict naming convention is the better compromise.
Know a case where this goes badly wrong? Tell us at info@widget-storm.de.
Try Widget Storm
The widgets run as live demos right on the page. Public widgets can be embedded freely into their own projects by any registered user.
Browse the widgets Support chat without an iframe: €4.99 one-time
Total price · no VAT (§ 19 UStG)