CSS resets and global styles in web components

Mar 3, 2022

If you’ve read my previous article on extracting Svelte Components as Web Components, you might have seen a note about problems to be solved with CSS resets and global styles. In this article I’ll explain the issue in more detail and how we’ve solved this problem altogether.

:root,
:host {
  /* Global styles that work in web components */
  font-family: Arial, sans-serif;
}

The problem

If you extract individual components or parts of your app (from any framework) into web components, they’ll work fine as long as they don’t depend on some global CSS that was defined on the project you’re using them in. The CSS I’m talking about is:

  • global variable definitions in :root {}
  • base font and text rendering definitions on the body {}
  • default behaviours for all elements like *, *::before, *::after { box-sizing: border-box; }
  • and other general CSS resets (I recommend using this minimal “reset by” Josh Comeau).

There are two obvious attempts to get these styles into your web component, but both don’t work:

Attempt 1) Import your global CSS on the website you’re using the web components

There are multiple reasons why this doesn’t work but the most obvious is: CSS doesn’t cross shadow DOM boundaries. So if you have a font defined on body then this font definition will not cascade into your web component (which is a good thing).

You might be tempted to forgo shadow DOM to avoid this issue (after all, web components do not need to live in shadow DOM), but then you would have style bleeds from the site into your Web Components (so you can’t be certain anymore that they’ll look correct in all circumstances), as well as affect the site you’re using it on since these global styles will affect everything there as well.

Attempt 2) Import your global CSS in your web components

This seems promising at first, but you’ll quickly run into a problem: if your global definitions define anything on :root or body it will not work in your Web Component because neither of these elements is defined in your shadow DOM.

The solution

Importing the global CSS in your web components is a great start. It allows your components to be completely autonomous, thus working as expected in any environment.

All selectors that address specific elements work out of the box (like button, a, p, h1, etc... and even *), but base definitions like fonts on body or CSS variables on :root do not.

Luckily there is a selector that works inside the Shadow DOM and can be used for the same thing.

:host to the rescue

Mozilla provides a great explanation on what the :host selector does so I’ll just copy it here:

The :host CSS pseudo-class selects the shadow host of the shadow DOM containing the CSS it is used inside — in other words, this allows you to select a custom element from inside its shadow DOM.

So all you need to do now, is to make sure that everywhere you use :root or body in your CSS, you also add :host to it.

Since :host “…has no effect when used outside a shadow DOM” your Svelte Kit app (or wherever you are building your components) will not be affected by this.

And since :root and body don’t select anything inside your shadow DOM, they will not have any adverse effect in your web components either.

CSS example

To give a simple example, here is what a very trivial CSS would then look like:

/*
  Josh's Custom CSS Reset
  https://www.joshwcomeau.com/css/custom-css-reset/
*/

*,
*::before,
*::after {
  box-sizing: border-box;
}
* {
  margin: 0;
}
/*
  Note that we didn't add :host here, because this
  would affect the layout of the web component
*/
html,
body {
  height: 100%;
}
/*
  Added :host because we also use web components
*/
body,
:host {
  line-height: 1.5;
  -webkit-font-smoothing: antialiased;
}
/* etc... */

/* Global app styles */
body,
:host {
  font-family: 'Great Font', sans-serif;
}

Getting the styles into your web component

There are multiple ways you can achieve this. A very trivial example is to simply add a <link rel=”stylesheet” href=”/global.css”> to your shadow DOM. But that means you’d also have to host it somewhere.

The route we went, is to import the CSS in the web component wrapper via rollup, and inject it in the shadow DOM at runtime:

// Import our global app CSS
import css from '../lib/style/app.css'
import MainMenuSvelteComponent from '../lib/components/MainMenu.svelte'

export abstract class MainMenu extends HTMLElement {
  constructor() {
    super()

    // Create the shadow root the svelte component is going to live in.
    const shadowRoot = this.attachShadow({ mode: 'open' })

    // Attach the app.css to the shadow root, to apply css resets, and default
    // properties.
    shadowRoot.innerHTML = `<style media="screen">${css}</style>`

    new MainMenuSvelteComponent({
      target: shadowRoot,
      props: {
        // Any props your component might need
      },
    })
  }
}

// This registers the element so it can be used as <main-menu></main-menu>
customElements.define('main-menu', MainMenu)

This has the disadvantage that the browser needs to parse the CSS every time a web component of yours is added, but if your CSS is not too big and you don’t use an excessive amount of web components, that shouldn’t matter. (Even if it were noticeably slower, our long term strategy is to replace the old frontend any way, so top performance is not the main goal here)

Because we compile all our web components into one web-components.min.jsthe CSS will not be duplicated in the file, since there is only one import of the CSS that is then injected for each individual component at runtime.


We have already used this practice in production and have been very happy with it so far. The fact that we are now able to inject the global CSS means that there are never any surprises when using a Svelte Component on another site.

It’s awesome being able to work on a new Svelte Kit site and replacing parts of the old frontend step by step for it to be fully replaced by Svelte Kit in the end.

Need a Break?

I built Pausly to help people like us step away from the screen for just a few minutes and move in ways that refresh both body and mind. Whether you’re coding, designing, or writing, a quick break can make all the difference.

Give it a try
Pausly movement