Creating and Integrating Design Systems with StencilJS

A step-by-step guide to building and distributing a design system built with StencilJS

I’ve recently written an article about the fundamentals of StencilJS and how to get started with it to write reusable Web Components.

In this article, instead, I want to deep-dive on the use-case that led to the creation of Stencil by the Ionic team: creating reusable design systems.

While it’s entirely possible to build fully-featured apps with Stencil alone, this toolchain makes an ideal companion for bigger frameworks for creating truly reusable components.

Indeed, it allows us to build components that are:

  • extremely small
  • performant out-of-the-box
  • reusable with any framework, or no framework at all
  • future-proof

In this article, we will see:

  • how to scaffold and build a Stencil components project
  • the details and the anatomy of a Stencil design system
  • leverage CSS variables to theme our components
  • how we can integrate the component with other frameworks such as Angular

Scaffolding a Components Project

The Stencil CLI helps us scaffold a new project without having to manually create any configuration.

To start, we run the following command:

npm init stencil

The CLI will prompt you what sort of project you want to build, and we can choose component

Once the project is created, we can simply run npm start and start tinkering with our components.

Anatomy of a Stencil Components Project

Let’s take a look at what Stencil generated for us, so we can understand better how it works under the hood.

Anatomy of a Stencil Components Project

The most important places to look at from the image on the left are the following: — /src/components/src/components.d.ts/stencil.config.js

The folder src/components contains all the components we’re going to build with Stencil.

The file components.d.ts contains the Typescript type declarations automatically generated by the Stencil compiler.

The Stencil configuration is used for setting up the outputs and passing some information to the compiler.

It needs almost no manual intervention.

In the next paragraphs, we will see how the distribution folder is created and how we can integrate it with other existing codebases.

Creating a Collection of Components

In order to run and try out the components, we don’t need to set up anything else. You can simply run the following command:

npm start

This command will call the Stencil CLI in watch and dev mode and will serve the file /src/index.html that we can use to test our components.

Creating our Component: my-button

Let’s start creating our first component, called my-button. This is the standard reusable button in our design system.

We create two files inside the folder components/button:

  • button.tsx
  • button.scss
import { Component, h } from "@stencil/core";

@Component({
  tag: 'my-button',
  styleUrls: ['./button.scss'],
  scoped: true
})
export class ButtonComponent {
  render() {
    return (
      <button>
        <slot />
      </button>
    );
  }
}

For the styles, let’s go with something minimal and simple:

button {
  background-color: #42a5f5;
  color: #fff;
  padding: 12px 24px;
  border-radius: 4px;
  outline: none;
  font-size: 1em;
  border-color: transparent;
  font-weight: 500;
  font-family: sans-serif;
  cursor: pointer;
}

Importing and Reusing Components

One of the most important things do take into account when building a design system is to create atom components that are composed to build larger complex components.

If you’re used to having to import or register components in order to instantiate them, then forget everything you know.

Stencil allows to seamlessly use components without the need to import them anywhere. We just need to create a class (export one single class per component) and the toolchain will take care of it.

To test how the button looks like, we open the file index.html and add the code required to display the component we just created:

<!-- index.html -->
<my-button>
  Click Here
</my-button>

That’s pretty much it! Below you can see the result:

Demo

Theming your Design System

Stencil promotes CSS Variables as a way to style components and create themes. If you have never used a CSS Variable, you’re in good company. I hadn’t used them until a couple of months ago, but I was impressed with their power.

Stencil allows to define a global style typically called global.scss. This file will normally contain styles that will be available to every element, but also to define the variables for our components.

CSS Variables are an ideal way to:

  • reuse values and ensure consistency among all our components
  • easily create multiple themes
  • allow consumers to easily customize with their own styles

Adding a Global SASS Style

In order to use this, we need to first install the SASS plugin:

npm i -D @stencil/sass

We add the plugin and the global.scss file to the configuration:

import { sass } from '@stencil/sass';
export const config: Config = {
  // more stuff...
  plugins: [sass()],
  globalStyle: './src/global.scss'
}

CSS Variables: a quick introduction

If you have used SASS variables in the past, there’s not much else to know except for slightly different syntax details.

To define a CSS variable, we simply define a value prefixed with --:

:root {
  --primary-color: #42a5f5;
  --text-color: #323232;
}

Once defined, we can use these anywhere in our CSS files by wrapping the variable name with var():

button {
   background-color: var(--primary-color);
}

Use Case: Light Theme vs Dark Theme

A common use-case of design systems is to provide multiple themes. Using CSS variables makes it an extremely easy task.

First, we change the properties of the button to use variables:

button {
  background-color: var(--button-background-color);
  color: var(--button-color);
  ...
}

Then, we can define the variables based on the selected theme. In order to achieve that, we use :host-context(selector). This pseudo-class function allows us to select the host based on the selector passed as a parameter.

:host-context(.light-theme) {
  --button-background-color: var(--primary-color);
  --button-color: #fff;
}

:host-context(.dark-theme) {
  --button-background-color: #424242;
  --button-color: var(--primary-color);
}

Changing themes is a matter of switching the correct theme class on one of the ancestors.

For example, we can add the class dark-theme to the body element.

Building the project for distribution

The Stencil compiler will intelligently generate many small files and will automatically import polyfills for browsers that don’t support certain features (for example, IE does not support CSS variables).

In order to run the project for distribution, we can run the following command:

npm build

Below you can see the structure of the generated files:

That’s a lot of files, but you only need to import three of them from your code:

  • the ESM bundle
  • the ES5 bundle
  • the CSS

How does Stencil help us ship fast bundles?

  • Stencil does not (currently) create a single bundle. Instead, it creates many small bundles that are requested by the components loaded on a page. This helps with loading only the resources that are actually needed. It’s also ideal to take advantage of HTTP/2.

  • Differential Bundling: Stencil will generate 2 bundles, one for modern browsers and one for older ones (ESM and ES5). Modern browsers will not download the polyfills needed to run cutting-edge features.

<script type="module" src="/build/design-system.esm.js"></script>
<script nomodule src="/build/design-system.js"></script>

Integrating Components with other Frameworks

There is still work to do, but I believe that nowadays we have the tools and enough browser-support to start writing our design systems entirely with Web Components, and Stencil is a great tool for doing it.

Whether your app is built with React, Angular or any other framework, one of the best ways to future-proof and reuse your components is to leverage Web Components and write wrappers (through slots, props, and events) in your framework(s) of choice.

Once your design system is ready to be consumed by your larger applications (built with anything else), it’s time to include the components generated by the Stencil compiler.

There are different ways to do that depending on how you structured your applications:

  • if you’re using a monorepo, you can import the bundles manually from the build folder
  • if you’re using multiple repositories, you can fetch your design system bundles your files via a CDN, or by using Bit

The Stencil Documentation has an exhaustive (yet brief, as it is a pretty simple task) for implementing Stencil projects with the major frameworks.