On Styling Web Components

Harshal Patil
webf
Published in
6 min readApr 8, 2019

--

Doing it the right way — Tapping into the CSS Object Model

CSS Object Model is different than DOM and it plays well with Shadow DOM.

Encapsulation is the primary reason why you might want to start using Web Components. In particular, Shadow DOM is the mechanism to achieve true encapsulation. Over the years, web community invented patterns to fix global nature of CSS and JavaScript. These solutions worked but they were not perfect. It took Shadow DOM to really provide the perfect scoping for our CSS.

In this article, we will explore ways to apply styles to web components and attempt to arrive at the most idiomatic way of doing it. Further, we will try to integrate our solution with today’s build tools.

The naive way — Plain <style> tag

When using Shadow DOM, for a style sheet to work, it currently must be specified using a <style> element within each shadow root. As such most of the articles around Web Components will illustrate this idea on its face value:

The solution is simple and straight-forward. However, there is a problem:

For each instance of a component added to the page, a browser will parse the style sheet rules.

This will have an impact on performance on both time and memory axes. Time increases because browsers need to parse the raw strings and Memory cost increases as style rules are stored for every instance of the component. Browsers do not have a way to know if two instances of the same component share the same styles.

Update: As Eric Bidelman pointed out in the comments, the performance aspect need not be really true. It is possible that browsers may internally perform an optimization so that it doesn’t have to parse style tag each time an instance is created. In fact, Blink (Chrome, Opera, etc.) engine has already optimized this.

Can we do something better?

Indeed, we can prevent re-parsing of the style rules by creating a style node and then deep cloning it for each instance of component:

Again, this approach is easy but still has problems.

  • First, it doesn’t really help us share the style across component instances. New style node is still created.
  • It is awkward to use it with declarative solutions like lit-html or hyperHTML.

Enter the Constructible Stylesheets

As the name suggests, constructible stylesheets allow to create and share styles when using Shadow DOM.

Using CSSStyleSheet() constructor
Using constructible stylesheets with Web Components and Shadow DOM

Besides solving the problems of duplicate copies, it has a few more characteristics:

  • Styles are not only shared by instances of the same component but also by the multiple web components.
  • It also has support handling asynchronous style. For example, when you have url or @import rules in your CSS codes.
  • Finally, adoptedStyleSheets is an array. It means you can really compose your reusable stylesheets in ways that we could not possibly do before. To start with, you can split your CSS rules in small chunks and only apply chucks that are required. You can even do something like:
shadowRoot.adoptedStyleSheets = [sheet];// Remove stylesheets after two seconds
setTimeout(() => shadowRoot.adoptedStyleSheets = [], 2000);

Using with Webpack and SCSS

SCSS as a CSS pre-processor and Webpack as a module bundler and a build tool is a pretty common setup. CSSStyleSheet.replace() and CSSStyleSheet.replaceSync() expects a CSS rules a raw string. We can use a simple loader chain of sass-loader and raw-loader as follows:

webpack.config.js

In your code you can then import SCSS file:

// Read SCSS file as a raw CSS text
import styleText from './my-component.scss';

const sheet = new CSSStyleSheet();
sheet.replaceSync(styleText);

A similar setup is possible with Rollup.js. Further, new library from Polymer Team — LitElement is already using this approach with fallback:

*Note: Currently constructible stylesheets are implemented in Chromium family of browsers. However, with reasonable progressive enhancement practices, it should be easy to provide support for non-supporting browsers.

Under the hood — CSSOM

*You can skip this section if you are not interested in finer details.

If HTML markup is transformed into a Document Object Model (DOM) then CSS markup is transformed into a CSS Object Model (CSSOM). These are both independent data structure. Final render tree constructed using these two data structures. As a front-end developer, it is not really important to understand the mechanics of CSS Object Model and thus it is a lesser known concept.

Roughly speaking, one <link type='text/css' href='' /> tag corresponds to one CSS stylesheet. It is represented by CSSStyleSheet interface in CSSOM. One CSSStyleSheet consists of multiple rules. Each rule is represented by CSSRule interface.

To access all the CSSStyleSheet objects applied to a given document, you can use document.styleSheets property. Traditionally, if you wanted to create a CSSStyleSheet object, you would have to create a style tag; add it to document and use sheet property:

const styleNode = document.createElement('style');// It is important to add style node to the document
document.head.appendChild(styleNode);
const sheet = styleNode.sheet;

However, constructible stylesheet enables two APIs — CSSStyleSheet() constructor and document.adoptedStyleSheets.

With constructible stylesheet, you can use CSSStyleSheet() constructor to create stylesheets using JavaScript.

Property adoptedStyleSheets is available on Shadow Roots and Documents. It allows us to apply the styles defined by a CSSStyleSheet to a given DOM sub-tree. Note that adoptedStyleSheets is an immutable array and thus push or splice will not work.

adoptedStyleSheets is the second part of the puzzle

As far as CSSRule is concerned, there are many types of CSSRule, all of which extends CSSRule interface. MDN provides a list of all these rules:

CSS Object Model is huge and continues to grow every day. You can find the detailed Object model here:

Also, do not forget that the new CSS Typed Object Model is being actively developed. It will surely have an impact on how to write our CSS in JavaScript. You can read more about it in the following article:

Side notes

Irrespective of how you approach Styling in the context of Shadow DOM, following things should be kept in mind:

  • You can use external styles with Shadow DOM with the help of @import statement.
  • Slot items can be styled by global CSS or container component stylesheets.
  • CSS Custom properties cross the Shadow DOM boundaries.

As such, these custom properties are the preferred mechanism to style Shadow DOM contents from the outer world.

  • Beside custom properties, you can use new CSS selectors like :host, :part, :theme to further provide styling customization from the outer context.
  • To a good extent, Style encapsulation with Shadow DOM is a good alternative to CSS-in-JS approaches.
  • Virtual components (Component without mount elements like Vue <router-view /> or React <Route /> ) are not possible with Web components.
  • Web components are a very good candidate for replacing your leaf components like Inputs, Panels, Cards, Pickers, etc.

In the next article, we will explore concerns and design aspects related to web components in general. Stay in touch for future updates.

Credits & Artwork:

Special thanks to Charushila Patil for all the illustrations.

--

--

User Interfaces, Fanatic Functional, Writer and Obsessed with Readable Code, In love with ML and LISP… but writing JavaScript day-in-day-out.