Introduction

We created a fully themeable chat UI that can be embedded in any website and has no effect on the parent page. Kanton Basel-Stadts Alva and RAMMS' Rocky AI are instances of that UI.

This blog post will show you what we learned about creating web components that do not influence the parent page. Here are the good, the bad and the ugly when working with web components.

The Good

These are the good parts of web components. They will lay the foundation for why you might use them.

Portability

Every system that can handle HTML can handle web components. A simple tag and a script will integrate it into any web framework. It doesn't even need to be a JavaScript framework.

<body>
  <your-webcomponent></your-webcomponent>
  <script src="path/to/your-webcomponent.js"></script>
</body>

Native Feel

IFrames are another way to embed UI into a page, and they are arguably easier to use. But the main difference is that web components feel more native to the page, since they directly integrate into the parent page's layout. This means you can use transparency, intrinsic sizing (size based on the web component's contents), and seamless event communication with the parent page.

Slots

With slots you can provide content that will be added at a specified point inside your web component.

In our chat UI, we used a slot to let integrators provide a custom loading spinner. This spinner needs to be visible immediately, before the full theme loads asynchronously.

Shadow DOM - Isolating Styles

A robust way to ensure that your styles do not affect the parent page is to use the Shadow DOM. Shadow DOM is a web component feature to add a boundary for styles. Styles applied inside the Shadow DOM never apply to the parent page.

Caveat: Inheritable CSS Properties

There is one exception to the isolation where CSS properties of the parent page apply to the web component.

These are the properties that pierce through the boundary:

  • Inheritable CSS properties like color, font-family, line-height
  • CSS custom properties like --my-var

In practice, we have found it helps to fully specify the common properties like fonts and color on every element. That way you will never be surprised by different styles on integration.

Vite

For bundling web components, we can highly recommend Vite. There are a lot of neat tricks you can apply while bundling. Here are the Vite features we used for our web component.

Inlining Assets

Vite's explicit inline handling feature allowed us to inline our external CSS files into the JS bundle.

import cssContentString from "./index.css?inline";

This feature will not only inline the raw content of the imported index.css. It will also resolve all CSS imports, apply PostCSS transforms, and even work with CSS preprocessors like SASS. While inlined CSS is not the most efficient for browsers to render, the benefit is that we can ship a single JS file.

Library Mode

The Vite library mode provides you with fine-grained control of how the bundle should behave. To enable the library mode just add the build.lib option in your Vite config.

The Bad

Not everything about web components is great though. Here are the bad parts.

SSR - Hard to Get Working

Server-side rendering will almost certainly not work. The rest of the page can still be rendered server-side, but the web component will only show up as a <your-webcomponent></your-webcomponent> tag. Its contents will only be rendered in the browser.

There is one experimental package by Lit Labs that tries to solve this, but we never tried it.

Tailwind - Not a Great Fit

Tailwind feels like a natural choice for web components, but it does not play well with them.

The core issue is twofold. First, Tailwind ships its own CSS reset (called Preflight), which overrides default browser styles. When injected into a page that does not use Tailwind, it potentially breaks the page. Shadow DOM could isolate this reset, but Tailwind is fundamentally not designed to work inside a Shadow DOM. Here is the discussion if you are interested.

There are some hacky workarounds, but we tried them and had no success getting them to work reliably.

Our recommendation is to only use Tailwind if you are guaranteed that the parent page also uses it, and then use web components without Shadow DOM.

The Ugly

Verbose Web Components API

The native web component API is verbose and hard to read. A simple counter component, for example, requires manually defining a class, attaching a shadow root, setting up innerHTML, and wiring event listeners in connectedCallback. This boilerplate adds up quickly. You can see examples of the API here.

Fortunately, web components make for a great compile target. Svelte and Vue directly support compiling to web components. React is a bit trickier, but totally doable as well. We used this approach for our chat UI, where the first iteration was built with React and the current one with Svelte.

Weird Quirks

Advanced web component features come with edge cases
that no documentation warns you about. Even Svelte,
which has excellent web component support, ships with a
notable list of caveats.

We even hit an undocumented edge case with slots in Svelte: the bundle script must
load after the component markup, or slotted content will
not render. An ugly wrapper for slots fixes the problem,
but quirks like this add up and slow you down.

Font Loading - Not Working Inside Shadow DOM

When authoring web components, you get into the habit of defining all stylesheet links etc. in the web component body. As you should, so they do not affect the parent page. But there is another annoying detail here: @font-face will not work when defined in the Shadow DOM. If your web component needs a custom font, you need to inject the font CSS into the parent page to make it work.

Conclusion

I do not want to end on this ugly note though. I really think there are cases where web components are the right choice, and in our case we would choose Svelte & web components again.

To help you get started, here is a Svelte starter template.