CSS resets and global styles in web components
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.js
the
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