Build Your Own Design System With StencilJS
Published: 2019-12-06 13:39:20

---
title: Build Your Own Design System With StencilJS
canonical_url: https://marmelab.com/blog/2019/12/06/build-your-own-design-system-with-stenciljs.html
cover_image: https://thepracticaldev.s3.amazonaws.com/i/0obr4nixbwtxqpetqyj6.png
published: true
description: Design Systems are an increasingly common practice in web development. Let's study how to do ours with StencilJS
---

_Note: This post was originally posted on [marmelab.com](https://marmelab.com/blog/2019/12/06/build-your-own-design-system-with-stenciljs.html)._

In this post, we will define **what StencilJS** is and **how we can use it** to build a Design System. As a pre-requisite, you should know that [StencilJS](https://stenciljs.com/) is not a framework. **It's a compiler that produces reusable Web Components** that can be embedded anywhere else.

## What is a Design System?

Many publishers have a **graphic charter** defining a set of rules about their visual identity.

Given the technological developments in our societies, **these rules increasingly and largely concern digital media**, particularly the web.

In the last few years, a consensus has emerged in the world of web development. Indeed, **most javascript frameworks are largely based on components**. Components offer many advantages such as logic reusability, centralized style and easy testing. Their use provides a rich and uniform browsing experience far away from the early 21th century.

As a result, a new trend has emerged, merging the principles of graphical charter and standalone UI components: that's the **Design System**.

<img src="https://marmelab.com/static/78bb146d4c30e8b4c6ce3be5025fff34/e0b4a/designsystem.jpg" alt="Design System" />

## What Is StencilJS?

As previously said, **StencilJS is not another web framework such as ReactJS or VueJS**. **StencilJS is a toolchain** that facilitates the building of reusable and scalable Design Systems based on the [Web Component](https://developer.mozilla.org/en-US/docs/Web/Web_Components) standard. It was created by the [Ionic Framework](https://ionicframework.com/) team especially for this purpose.

Although totally different from a frontend framework, StencilJS uses many well-known concepts and technologies from frontend development - and that's often confusing for a developer. So, StencilJS provides:

- A virtual DOM
- JSX (like in ReactJS)
- Reactive Data (like AngularJS $watch)
- Async rendering (inspired by React Fiber)
- [TypeScript](https://www.typescriptlang.org/) support

By combining all these features, StencilJS is able to generate **standard-compliant** components. Moreover, StencilJS automatically adds the polyfill required to **support older browsers**. Here is the browser support grid provided by the StencilJS website.

<img src="https://marmelab.com/static/a86f73bc6b71b6bac88d0b104d5e2dd7/624ba/browser_support.png" alt="Browser Support" />

So, to sum up, using StencilJS allows you to **build a Design System that is ready for the future** thanks to its compliance with upcoming standards, its automatic polyfill and its advanced API.

## A Simple Case Study

In this section, I will detail the creation of a Design System from scratch using StencilJS. For the example, I will create a design system for Marmelab called `mml`. Here is the initialisation process below.

<img src="https://marmelab.com/4de6bb6a70aeeb00bf9f35909c5d1211/mml.gif" alt="Marmelab Design System" />

### Create a component

Once our project is set up, we can create our first component, in our case a "Github Card". StencilJS provides a special npm script command for that purpose, `generate`.

```bash
julien@julien-P553UA:~/Projets/marmelab/mml$ npm run generate
> mml@0.0.1 generate /home/julien/Projets/marmelab/mml
> stencil generate

✔ Component tag name (dash-case): … github-card
✔ Which additional files do you want to generate? › Stylesheet, Spec Test, E2E Test

$ stencil generate github-card

The following files have been generated:
 - src/components/github-card/github-card.tsx
 - src/components/github-card/github-card.css
 - src/components/github-card/github-card.spec.ts
 - src/components/github-card/github-card.e2e.ts
```

**StencilJS has generated all the required files** to create our component, even the test files! Here are the generated files from that command (excluding tests).

```js
// ./src/components/github-card/github-card.css

:host {
  display: block;
}

// ./src/components/github-card/github-card.tsx

import { Component, Host, h } from '@stencil/core';

@Component({
  tag: 'github-card',
  styleUrl: 'github-card.css',
  shadow: true
})
export class GithubCard {
  render() {
    return (
      <Host>
        <slot></slot>
      </Host>
    );
  }
}
```

Now you might think: We're supposed to use StencilJS, how come this command has generated Angular code? Well, although it looks like Angular code, it's actually StencilJS. And I agree, the annotations at the beginning of the module can be confusing. But don't worry, you're on the right track ;)

So, the `@Component` annotation allows us to declare a StencilJS component, which can be configured using several options:

- `tag`: name of the tag on which our component will be registered.
- `styleUrl`: relative url to the corresponding style file
- `shadow`: enable the browser [Shadow DOM](https://developer.mozilla.org/fr/docs/Web/Web_Components/Using_shadow_DOM) encapsulation
- [other options...](https://stenciljs.com/docs/component#component-options)

In the `render` method, the `Host` component represents the root of the component, the tag itself. Within it, the `slot` allows to inject children in our custom element as described below.

```html
<github-card>
   <div>I'm a children, and i'll replace the <!-- <slot>--></div>
</github-card>
```

Since we use Shadow DOM, only the styles inside our `github-card.css` tag will have an impact on the display of our `github-card`. The special `:host` selector refers to the tag itself (aka `Host`).

Then, our component exists and we're able to see it (not much at the moment) in the browser using `yarn start` (aka `stencil build --dev --watch --serve`).

If we inspects the DOM using chrome `devtools`, this is what we'll see.

<img src="https://marmelab.com/static/3b306b1d4880b454db19232614772e2c/c3508/dom.png" alt="StencilJS DOM" />

After a few minutes in the shoes of an integrator, **here is what I get from some html and css**. Sadly, everything is static for the moment, and I have much less than 42 followers in real-life ^^.

<img src="https://marmelab.com/static/947737eab8eb32e4869c011bbca7d083/3d9f8/basecard.png" alt="HTML Github Card" />

In the next chapter, I'll explain how to **configure which user is displayed** using a special `login` attribute on our custom element.

```html
<github-card login="jdemangeon"></github-card>
```

### Passing Props To A Component

Like [ReactJS](https://reactjs.org/), StencilJS provides `state`, `props` and `lifecycle hooks`. So, the first step is to declare a `login` prop and an `user` state in our component. Whereas `login` will receive the name of the GitHub user, `user` will receive the user object coming from the GitHub API.

```diff
- import { Component, Host, h } from "@stencil/core";
+ import { Component, Host, h, Prop, State } from "@stencil/core";


@Component({
  tag: "github-card",
  styleUrl: "github-card.css",
  shadow: true
})
export class GithubCard {
+  @Prop() login: string;
+  @State() user: any;

  render() {
    ...
-     <a class="avatar" href={`https://github.com/jdemangeon`}>
+     <a class="avatar" href={`https://github.com/${this.login}`}>
    ...
  }
}
```

Contrary to React, there's no `this.props` object to access props with StencilJS. Prop values are directly attached to the `this` instance. So we can access `login` using `this.login`.

So, our links are up to date, and point to the right profile according to the `login` prop. Other data such as the number of followers, repositories, etc... are not dynamic and require a call to the Github API.

### External And Internal APIs

I don't really want to expose my GitHub popularity in the spotlight... But it's the lottery, honey! So, in this section we'll explore how to call the GitHub API from the component, and display real values based on the response.

First, **we will declare a function that calls Github** and assigns values to our user. And in a second step, we will **call this function when mounting the component**.

```diff

export class GithubCard {
  @Prop() login: string;
  @State() user: any;

+ async componentWillLoad() {
+   return this.fetchUser(this.login);
+ }

+ async fetchUser(login: string) {
+   const response = await fetch(`https://api.github.com/users/${login}`);
+
+   if (response.status === 200) {
+     this.user = await response.json();
+   } else {
+     this.user = null;
+   }
+ }
```

Then, we are able to use the user information inside the `render()` method. If the user doesn't exist or is not retrieved yet, we'll return `null`.

```diff
  render() {
+   if (!this.user) {
+     return null;
+   }

    return (
      <Host>
        <div class="card">
          <div class="header" />
          <a class="avatar" href={`https://github.com/${this.login}`}>
-           <img src="https://avatars0.githubusercontent.com/u/1064780" alt={this.login} />
+           <img src={this.user.avatar_url} alt={this.login} />
          </a>
          <div>
-           <h1>Julien Demangeon</h1>
+           <h1>{this.user.name}</h1>
            <ul>
              <li>
                <a
                  target="_blank"
                  href={`https://github.com/${this.login}?tab=repositories`}
                >
-                 <strong>42</strong>Repos
+                 <strong>{this.user.public_repos}</strong>Repos
                </a>
              </li>
              ...
```

<div class="tips">

Just like props, state attributes are directly attached to the instance through `this`. So you have to be very careful with naming to avoid conflicts between state and props.

</div>

So yeah, it works. But what happens if I update the `login` attribute?

```diff
- <github-card login="jdemangeon"></github-card>
+ <github-card login="marmelab"></github-card>
```

Well, my component doesn't change... but why? Because the `componentWillLoad` lifecycle method which is used is called only once, just before the component mounts to the DOM. To reflect prop (and state) changes, we must implement `componentWillUpdate`, too.

```diff
export class GithubCard {
  @Prop() login: string;
  @State() user: any;

  async componentWillLoad() {
    return this.fetchUser(this.login);
  }

+ async componentWillUpdate() {
+   return this.fetchUser(this.login);
+ }
```

<div class="tips">

`componentWillLoad`, `componentWillUpdate` and `componentWillRender` lifecycle methods are special. They can return a [Promise](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Promise) that can be used to wait for the next render.

</div>

YES! Our component no shows user information, and changes when the `login` prop changes. What if we want to allow developers  to display other information? To achieve this goal, we'll use a `slot`.

```diff
   return (
      <Host>
        <div class="card">
          <div class="header" />
          <a class="avatar" href={`https://github.com/${this.login}`}>
            <img src={this.user.avatar_url} alt={this.login} />
          </a>
          <div>
            <h1>{this.user.name}</h1>
+           <slot />
            <ul>
              <li>
              ...
```

A little change in the html...

```diff
- <github-card login="jdemangeon"></github-card>
+ <github-card login="jdemangeon">
+   <span>I like Pastis!</span>
+ </github-card>
```

And tada! Isn't life beautiful? In this case there's only one `slot`, but you need to be aware that multiple `slots` can be used thanks to [named slots](https://stenciljs.com/docs/templating-jsx#slots).

<img src="https://marmelab.com/static/d83f40b3a9d4ae86a0bdb4da8cf80a38/c39d3/ilikepastis.png" alt="I like Paris" />

In reality, if you apply the example as it is, you will see the text "I like pastis" blinking before the rest of the component. This is quite normal, since we use "normal" html tags and because the browser will show the text before javascript execution.

<img src="https://marmelab.com/5b661110fad1a04d3fcee8786f97a8bc/cloak.gif" style="display: block; margin: 0 auto" />

To avoid this problem, StencilJS will apply an `hydrated` class on the components once they are mounted. So it's necessary to declare the style below to avoid the problem.

```html
    <style type="text/css">
      github-card {
        display: none;
      }
      github-card.hydrated {
        display: block;
      }
    </style>
```

<div class="tip">

If you've already used [VueJS](https://vuejs.org/), you probably already known the [v-cloak](https://medium.com/vuejs-tips/v-cloak-45a05da28dc4) attribute, which is based on the same principle. The same applies to AngularJS with `ng-cloak`.

</div>

The StencilJS API provides a [lot of features](https://stenciljs.com/docs/decorators) that would be difficult to cover completely here. I can't recommend enough that you take a look at the official documentation, which is very extensive.

### Composing Components

We have built a standalone component. **How can we interact with it from other components?** We're going to create a user selector, which allows us to change the user in the card.

So, we use the same `generate` command with a `github-card-selector` this time. And here is the finalized component below.

```ts
// src/components/github-card-selector/github-card-selector.tsx

import { Component, Host, h, State } from "@stencil/core";

@Component({
  tag: "github-card-selector",
  styleUrl: "github-card-selector.css",
  shadow: true
})
export class GithubSelector {
  @State() login: string;

  handleLoginChange(e: UIEvent) {
    const target = e.target as HTMLInputElement;
    this.login = target.value;
  }

  render() {
    return (
      <Host>
        <input
          onChange={this.handleLoginChange.bind(this)}
          placeholder="Github username"
        />
        {this.login && <github-card login={this.login} />}
      </Host>
    );
  }
}
```

As we can see, the code is simple enough to be readable. However, it presupposes that the `github-card` html tag is already registered on the browser (thankfully, StencilJS takes care of that when the mml library is loaded). **There is no explicit import.** Of course, since we use `TypeScript`, everything is typed, even [UIEvent](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent) and [HTMLInputElement](https://developer.mozilla.org/en-US/docs/Web/API/HTMLInputElement) browser events.

<img src="https://marmelab.com/0de8a57dd9da48e9a92a0e9a4bf430db/githubselector.gif" style="display: block; margin: 0 auto" />

You can find the project sources at the following address: [https://github.com/marmelab/mml](https://github.com/marmelab/mml).

## Component Testing

There are 2 different test types in StencilJS: **Unit Tests** and **End-To-End** (e2e). StencilJS uses [Jest](https://jestjs.io/) for unit tests and [Puppeteer](https://github.com/GoogleChrome/puppeteer) for e2e tests. If you already know these two excellent tools, you won't be lost.

<img src="https://marmelab.com/static/a4ddc82556f1da2bfdef127ab01f6324/624ba/jestpuppeteer.png" alt="Jest And Puppeteer" />

Thanks to Puppeteer, it is **possible to fine-tune the configuration of browser tests**. This includes `touch management`, or `landscape mode`, up to `viewport emulation`. Using unit tests, it is also possible to mock many features such as `HTTP Referer`, `Cookies`, `Url`, and so on.

In fact, I didn't find any significant difference between these two test modes. One is just slower because it passes through Puppeteer, but does not bring any added value. I think **it's better to focus on e2e tests once the component is integrated into the final application**.

For more information on testing with StencilJS, the documentation can be found [here](https://stenciljs.com/docs/testing-overview).

## Framework interoperability

As already explained, StencilJS is not a framework, it's just a web-components compiler. Although dedicated to this unique task, **StencilJS's underlying objective is to enable end-to-end development of web applications** based on the new standards.

It's still difficult to build a complex application entirely with StencilJS. For this reason, **StencilJS provides a set of functions that allow components to be injected directly into existing web applications**. Here is an example with ReactJS below.

```js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import { applyPolyfills, defineCustomElements } from '<your-design-system-lib>/loader';

ReactDOM.render(<App />, document.getElementById('root'));

applyPolyfills().then(() => {
  defineCustomElements(window);
});
```

Not all frameworks allow easy integration of custom elements (web-components). [A website](https://custom-elements-everywhere.com/) lists the compatibility rate for each of them.

## Design System Target

One of the greatest feature of StencilJS is that it can generate various builds at the same time to cover all expected needs and targets.

So, StencilJS can generate both the [ES5](https://en.wikipedia.org/wiki/ECMAScript#5th_Edition) and [ECMAScript Modules (esm)](https://medium.com/passpill-project/files-with-mjs-extension-for-javascript-modules-ced195d7c84a) versions for each component. It can also generate the corresponding documentation in `markdown` or `json`.

We can configure that targets through the `stencil.config.js` this way.

```js
import { Config } from '@stencil/core';

export const config: Config = {
  namespace: 'mml',
  outputTargets: [
    {
      type: 'dist',
      esmLoaderPath: '../loader'
    },
    {
      type: 'docs-readme'
    },
    {
      type: 'www',
    }
  ]
};
```

From the `www` output target folder, we can directly test our component by serving it. You can test it directly in the [original blog post](https://marmelab.com/blog/2019/12/06/build-your-own-design-system-with-stenciljs.html#design-system-target). Type your Github username, press Enter, and Voilà!

Here is the [output target](https://stenciljs.com/docs/output-targets) documentation.

## Conclusion

**StencilJS cannot replace web frameworks** such as ReactJS or VueJS. Indeed, Web Components are unable to receive complex attribute data (aka `prop` in component terms). Like any html tag, they can only receive scalar / textual data (aka `attribute`).  This makes their use very limited.

Some have used [hacks](https://github.com/alesgenova/stenciljs-in-react#appendix-attribute-vs-prop) to get around this limitation, but I'm not sure the game is worth it.

Therefore, **StencilJS is a very good choice if you want to create graphical components without logic** (dumb components) in an application or if you want to create widgets that can be integrated out of context everywhere.