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.
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:
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.
I'll probably never write again a reusable component with Angular/React/Vue. I'll simply write it with @stenciljs and then write wrappers around it.
— Giancarlo Buomprisco (@gc_psk) February 10, 2020
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.