Build an Embeddable Widget using Preact and the Shadow DOM
Tags: preact, shadowdom, webcomponents, styledcomponents
Published: 2021-10-13 21:36:50

 ---
title: Build an Embeddable Widget using Preact and the Shadow DOM
published: true
description: How to build embeddable JavaScript widgets using preact that is encapsulated with the Shadow DOM API. 
tags: preact, shadowdom, webcomponents, styledcomponents
---
Our team at [CompanyCam](https://companycam.com/) was tasked with building a widget that our users could embed on their websites. The widget needed to be easy to install, responsive, and provide a fullscreen application experience. This article introduces and explains the technical decisions made and how we got there.

## Discovery

Before jumping into the code, I want to quickly discuss some things our team learned during discovery. Hopefully, this will assist you in making the right decisions for your project.

After learning about the details of the product, we found that the codebase had two requirements.

### Encapsulation

Our team needed to prevent external CSS from cascading into our code. In addition, our styling needed to be scoped to our application. We explored wrapping the widget in an iFrame, which provides [a nested browsing context](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe).  This offered the encapsulation we needed, but we found it difficult to control the iFrame in order to provide a quality fullscreen experience. The [Fullscreen API](https://developer.mozilla.org/en-US/docs/Web/API/Fullscreen_API) was a potential solution, but it did not hold the required [browser support](https://caniuse.com/fullscreen). Using an iFrame to encapsulate a smaller product could be a great solution, but did not fit our use case.

We turned our attention to the [Shadow DOM API](https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM). The Shadow DOM provides a way to attach a hidden DOM tree to any element. This creates encapsulation, but doesn't limit your ability to have control of the application. In addition, the Shadow DOM API has [good browser support](https://caniuse.com/?search=attachShadow). 

### Small Bundle

It's imperative that the widget loads *quickly*. With the strategy the team had in place, it was clear that it was going to be difficult to code-split our application. At CompanyCam, engineers write user interfaces in [React](https://reactjs.org/) therefore it made sense to stick with that.

As we added 3rd party libraries, our bundle size grew. We found that [Preact](https://preactjs.com/) was a good solution to this problem. It provides all the same features as React, but in a much smaller package. You can compare the unpacked size of [Preact](https://www.npmjs.com/package/preact) to a combined [React](https://www.npmjs.com/package/react) and [React-DOM](https://www.npmjs.com/package/react-dom) and see a significant difference!

Now, let's jump into some code! Feel free to clone [this starter repo](https://github.com/colemanimhoff/preact-shadow-dom-starter) if a working example is helpful for you.

## Mounting Your App with a Shadow DOM Layer

Preact is easy to [integrate into an existing project](https://preactjs.com/guide/v10/getting-started/#integrating-into-an-existing-pipeline). Mounting our Preact `App` component should look similar to React.

```js
/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

const appRoot = document.querySelector("#app-root");

render(<App />, appRoot);
```

Now let's add a Shadow DOM layer.

```jsx
/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

// app shadow root

const appRoot = document.querySelector("#app-root");
appRoot.attachShadow({
  mode: "open",
});

render(<App />, appRoot.shadowRoot);
```

We can attach a Shadow DOM layer to a regular DOM node, called a shadow host. We can do this by calling the `attachShadow` method, which takes `options` as a parameter. Passing `mode` with the value `open` allows the shadow DOM to be accessible through the `shadowRoot` property. The other value for `mode` is `closed`, which results in `shadowRoot` returning `null`. 

To verify things are in working order, we can open our 
browser's developer tools and and look at the DOM tree. Here, we can see our Shadow DOM layer.

![Screen Shot 2021-09-30 at 2.01.52 PM-1](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ijtt3i0jyy1gntsbxfz8.png)

## Styling the Shadow DOM
 
Styles must be scoped inside the Shadow DOM in order to render.

```js
/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

import styles from "./styles.css";

// app shadow root

const appRoot = document.querySelector("#app-root");
appRoot.attachShadow({
  mode: "open",
});

// inject styles

const styleTag = document.createElement("style");
styleTag.innerHTML = styles;
appRoot.appendChild(styleTag);

render(<App />, appRoot.shadowRoot);
```
If you're using webpack, keep in mind you will need [css-loader](https://webpack.js.org/loaders/css-loader/) in order for this approach to work. Create a `style` tag and set its `innerHTML` to an imported stylesheet.

![Screen Shot 2021-10-07 at 10.44.27 AM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1vl4im01rjq69ia45zxz.png) 

As our application grew, managing our styles became cumbersome and our team wanted to find another solution. At CompanyCam, our designers enjoy designing our products with [styled-components](https://styled-components.com/). With `styled-components`, a [generated stylesheet is injected](https://styled-components.com/docs/advanced#existing-css) at the end of the `head` of the document. Due to our Shadow DOM layer, this won't work without some configuration.

```jsx
/* @jsx h */
import { h } from "preact";
import styled, { StyleSheetManager } from "styled-components";

const Heading = styled.h3`
  color: #e155f5;
  font-family: sans-serif;
`;

const App = () => {
  return (
    <StyleSheetManager target={document.querySelector("#app-root").shadowRoot}>
        <Heading>Hey, Shadow DOM!</Heading>
    </StyleSheetManager>
  );
};

export default App;
```

The [StyleSheetManager](https://styled-components.com/docs/api#stylesheetmanager) helper component allows us to modify how styles are processed. Wrap it around the `App` component's `children` and pass the `shadowRoot` of the shadow host as the value of `target`. This provides an alternate DOM node to inject styles into.

Just like the previous technique, we can see our styles scoped within the Shadow DOM.

![Screen Shot 2021-10-07 at 10.53.48 AM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y01nycmwjzl6bnn1onwo.png)

## Avoid Inheritance

The Shadow DOM will prevent outside CSS selectors from reaching any contained markup. But, it is possible for elements in Shadow DOM to [inherit CSS values](https://www.w3.org/TR/css-scoping-1/#shadow-cascading). We can [reset properties to their default values](https://developer.mozilla.org/en-US/docs/Web/CSS/initial) by declaring the the property `all` to the value `initial` on the parent element of your application.

```jsx
/* @jsx h */
import { h } from "preact";
import styled, { StyleSheetManager } from "styled-components";

const Heading = styled.h3`
  color: #e155f5;
  font-family: sans-serif;
`;

const WidgetContainer = styled.div`
  all: initial;
`;

const App = () => {
  return (
    <StyleSheetManager target={document.querySelector("#app-root").shadowRoot}>
      <WidgetContainer>
        <Heading>Hey, Shadow DOM!</Heading>
      </WidgetContainer>
    </StyleSheetManager>
  );
};

export default App;
```

## Win the Stacking Order Battle with Portals

Whether it's Wordpress, Squarespace, Wix, or something from scratch, our widget needed to live on any website. Since [stacking order depends on the DOM tree hierarchy](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context#the_example), we immediately saw `z-index` issues in our fullscreen components.

![Screen Shot 2021-10-07 at 11.57.11 AM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/httqp2k5lo82jwxj8zll.png)

[Portals]((https://reactjs.org/docs/portals.html)) provide a way to render `children` into a DOM node which exists outside the context of the application. You can mount your `Portal` to any DOM node. In our case, we needed to render these fullscreen components as high in the DOM tree as possible. Therefore, we can append our `Portal` to the `body` of the document we are installing the widget on.

![Screen Shot 2021-10-07 at 11.57.17 AM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bkpk08269n5e2zltlu7n.png)

Let's create our `Portal` by starting at the root of our application.

```js
// index.js

/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

// shadow portal root

const portalRoot = document.createElement("div");
portalRoot.setAttribute("id", "portal-root");
portalRoot.attachShadow({
  mode: "open",
});
document.body.appendChild(portalRoot);

// app shadow root

const appRoot = document.querySelector("#app-root");
appRoot.attachShadow({
  mode: "open",
});

render(<App />, appRoot.shadowRoot);
```

Create a shadow host for the `Portal` component and give it an `id`. Then, just like we did with `appRoot`, attach a new Shadow DOM layer.

```jsx
/* @jsx h */

import { h, Fragment } from "preact";
import { useLayoutEffect, useRef } from "preact/hooks";
import { createPortal } from "preact/compat";
import styled, { StyleSheetManager } from "styled-components";

const Portal = ({ children, ...props }) => {
  const PortalContainer = styled.div`
    all: initial;
  `;

  const node = useRef();
  const portalRoot = document.querySelector("#portal-root");

  useLayoutEffect(() => {
    const { current } = node;
    if (current) {
      portalRoot.appendChild(current);
    }
  }, [node, portalRoot]);

  return (
    <Fragment ref={node} {...props}>
      {createPortal(
        <StyleSheetManager target={portalRoot.shadowRoot}>
          <PortalContainer>{children}</PortalContainer>
        </StyleSheetManager>,
        portalRoot.shadowRoot
      )}
    </Fragment>
  );
};

export default Portal;
```
Next, create the `Portal` component. Add an effect to append  `portalRoot` to the parent element of the component. From there, pass `children` and `portalRoot.shadowRoot` to `createPortal`.

Remember to scope your styles to the `Portal` Shadow DOM layer using `StyleSheetManager` and reset child elements' styles to their default values.

```jsx
/* @jsx h */
import { h } from "preact";
import styled, { StyleSheetManager } from "styled-components";

import Portal from "./Portal.jsx";

const Heading = styled.h3`
  color: #e155f5;
  font-family: sans-serif;
`;

const WidgetContainer = styled.div`
  all: initial;
`;

const App = () => {
  return (
    <StyleSheetManager target={document.querySelector("#app-root").shadowRoot}>
      <WidgetContainer>
        <Heading>Hey, Shadow DOM!</Heading>
        <Portal>
          <Heading>Hey, Shadow Portal!</Heading>
        </Portal>
      </WidgetContainer>
    </StyleSheetManager>
  );
};

export default App;
```

Now, we can wrap any fullscreen component within our `Portal`.

![Screen Shot 2021-10-07 at 4.09.34 PM](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ork0tbw48vvafvuq7d44.png) 

## Conclusion

Recently, our team has [released the widget to GA](https://companycam.com/showcases). The techniques outlined above have allowed us to build a rich application experience with a small codebase that is... *mostly* encapsulated. We still run into the occasional `z-index` issue or JavaScript event conflict provided by a website builder theme. Overall, widget installs have been a success.