The shadow DOM is typically associated with Web Components, but its style encapsulation properties can also be useful on its own. Up until recently, React's event system presented problems in the Shadow DOM, but those issues have been resolved in React 17. So while this post focuses on Preact since its small size is a good fit for the cases that style encapsulation is also useful, the same process will also work with React.
The main reason to use the shadow DOM is for style encapsulation. CSS rules do not cross the shadow DOM in either direction, although inherited properties are still inherited as usual (for instance, font-family, color, etc).
The above example demonstrates the style encapsulating properties of the shadow DOM. The red box is in the normal light DOM and the purple box contents are in a shadow DOM. Even though there is a style rule in the
index.html file to set the background-color of all buttons to red, it does not affect the button that is in the shadow DOM. Conversely, the style set in the shadow DOM to set the color of all
p tags to purple and
font-weight to bold does not affect the paragraph in the light DOM
For most apps, this sort of encapsulation is not necessary. Assuming you are in full control of all app styles, you can ensure the styles do not interfere. Style encapsulation can be incredibly useful, however, if you are building something that gets embedded onto host pages that you do not control. For instance the Grow.me, OneSignal or Intercom widgets (note that not all of them use shadow DOM). In these cases, the style encapsulation behavior the shadow DOM provides is very useful.
Rendering Preact or React into the shadow DOM is pretty simple. The target element that the initial Preact render call attaches to just needs to be within a shadow DOM.
That's all there is to it.
For the most part, everything works normally. I have, however, come across a few cases that required extra consideration.
By default, styled-components injects styles into the head node. When rendering components into the shadow DOM, this doesn't work since those styles can't cross the shadow DOM barrier. Luckily, styled-components provides a StyleSheetManager component that allows customizing the target node that the styles are injected into. Setting the target to the root element inside the shadow DOM works.
Click events still bubble out of the shadow DOM, but the events are retargeted when observed outside of the originating shadow DOM. One case where this is particularly problematic is menu libaries that setup click listeners on
window to determine if you click outside of the menu and automatically close it. The target ends up being the shadow DOM root when observed from the window event listener and that logic likely no longer functions properly.
The cost of that full encapsulation is a lot of overhead when it comes to interacting with the host site or perhaps other iframes if your embedded app requires multiple widgets. The postMessage API is great for cross-frame communication, but not having to communicate across frames at all is a whole lot less hassle. If your application doesn't demand the guarantees Iframe's provide, I think using the shadow DOM is preferrable.
When I read Shadow DOM, it is always in the voice of a Yugioh villain.