How Shadow DOM and <dialog> Saved Me
This new component library almost turned to trash
Internally we needed a way to let other orgs inside our company use our UI elements across their sites.
I built a component library using bun, and also a way to auto generate a script tag version along side the npm package. Bun test, bun build, etc made it possible to build this with zero runtime deps other than react, and incredibly fast test running and other dev-ex with a single dev dependency.
This allows other teams to include a script tag, and render our widgets, regardless of their frontend architecture setup.
After getting the authentication working for our partners (which is also pretty cool and a little different even though it uses a common jwk standard, maybe a future blog post on this one day) as soon as they rendered the script, immediately there were some style problems.
Naively, I assumed our partners would not have classes that globally override things… For example I noticed a class that said “if a class on the page has the word “grid” inside it” then it sets certain attributes on it.
I was thinking just using Tailwind, with a class name prefix would work great… haha, joke was on me.
I really didn’t want to iframe this, and really wanted this widget to feel like a natural part of other sites, where the end user can’t even tell it isn’t.
Thankfully a bit of research and talking time with claude opus, (literally 2 minutes or less) helped me remember we standardized the shadow dom almost a decade ago! It’s also been stable in most browsers since 2020. I had heard about it before but never had a reason to use it. I’ve been lucky enough to work on a lot of sites where our team had full control.
Style overrides are an anti-pattern in almost all cases, but this one is a valid case where it can exist and has to be solved.
Shadow DOM
Currently, our script tag setup looked something like this:
<script type="module" src="https://componentlibrary.com/index.js"></script>
<script type="module">
const { component, render } = UI;
const widget = document.getElementById("widget")
render(
widget,
component(
"WidgetName",
),
);
</script>I needed a way to isolate these styles without burdening the implementation team further than this simple API I already came up with. Turns out, shadow dom to the rescue:
<script type="module" src="https://componentlibrary.com/index.js"></script>
<script type="module">
const { component, render } = UI;
const shadow = document.getElementById("widget").attachShadow({ mode: "open" });
// Theme CSS must be inside the shadow root for style isolation
shadow.innerHTML = '<link rel="stylesheet" href="https://theme.com/style.css" /><div id="root"></div>';
render(
shadow.getElementById("root"),
component(
"WidgetName",
),
);
</script>As easy as changing this to attachShadow on that original element. Then, in the innerHTML we load the widget’s css file there.
The other team did this within 15 minutes after sharing the script change.
Now the widget rendered as expected! What a quick fix.
But… this widget has a side modal that needs to span the height of the entire page.
The next realization was, this partner had a 9999999999 !important z index on the header!
How can our library display on top of something like that, without requesting changes from the other org?
Dialog element
Thank god for this thing. Take a look at how this element is able to use a special “top layer” from Google chrome teams “what is the top layer” article. This system is a great idea. Years ago many of you may remember that React didn’t have a modal system built in, so if you wanted to co-locate a modal where your nested element is that triggers it, you ran into problems like this all the time.
Thanks to this new element that is native to the browser, it doesn’t matter where your node is, it can now be on top of everything else.
I had never needed to use this before, as half a decade back, React added a way to render to the top of the dom with it’s modal API. The problem with that is, I would need to make an element in the top layer of the shadow dom. I didn’t want to add more setup code, or automatic code that did this.
The dialog element solves it simply. I switched the side modal to use this html element, and now it shows over this high z-index header easily.
Conclusion
Thanks to these modern web apis, and AI agentic tools, kids these days will never know the pain of supporting IE 11 for what felt like 26 years, with its inability to do anything modern.
I was able to go from rendering a broken widget on a partner staging instance, to 1 hour later having both of these solved in a simple manner. I am incredibly thankful for auto updating browsers, and some of these more modern apis that have been added in recent years.


