How to Create a Component Library with Storybook and Stencil


Posted on Sat, Aug 12, 2023 typescript storybook node react stencil pnpm

Storybook and Stencil are powerful tools for building reusable UI components. In this guide, learn how to leverage them to create a living component library.

We'll build components with Stencil's Web Component compiler, then document and design them in Storybook's interactive workspace. See how these complementary technologies enable efficient component-driven development.

You'll have a shareable library of documented components in no time !

Tools Used Throughout The Article πŸ’­

We used the following tools in this guide

In the next section, we’ll look at these tools in detail and why we used them in the project.

So, What Is Stencil ? ⚑

Stencil is a modern web development tool that serves as a compiler for building web components. Unlike traditional frameworks, Stencil focuses on compiling reusable, scalable, and standard-compliant components that can work seamlessly across different frameworks and libraries.

It's like the Swiss Army knife of web development, offering a range of functionalities without locking you into a specific ecosystem.

Why Would You Use Stencil Over Other Tools? 🀷🏼

1. Framework Agnostic πŸ”

One of the most compelling reasons to choose Stencil is its framework-agnostic nature. Whether you're working with Angular, React, or Vue, Stencil components can be easily integrated, offering you the freedom to switch between frameworks without rewriting your code.

2. Performance 🚀

Stencil is designed for speed. It uses lazy-loading and optimizes components for maximum performance, ensuring that your website runs smoothly and loads quickly.

3. Standards Compliant πŸ—ƒοΈ

Stencil adheres to Web Components standards, making it future-proof. This means you don't have to worry about your components becoming obsolete as web technologies evolve.

4. Ease of Use πŸŽ—οΈ

With a simple and intuitive API, Stencil is incredibly user-friendly. Even if you're new to web development, you'll find it easy to get started. It also offers excellent documentation and community support.

5. Flexibility and Customization 🎨

Stencil provides a high degree of customization, allowing you to create components that fit your specific needs. Its flexibility extends to theming, state management, and even server-side rendering.

What is Storybook ? πŸ€”

Storybook is an open-source tool that provides a development environment for UI components. It enables developers to create components independently and showcase them interactively in an isolated sandbox.

This means you can develop one component at a time, test it with various states and combinations, and ensure it's working as expected before integrating it into your application.

Storybook supports various frameworks, including React, Vue, Angular, and many more, making it versatile and adaptable to your existing tech stack.

Why Use Storybook For A Project? 🀷🏼

Below are few reasons why Storybook is an essential tool in modern frontend development

1. Enhanced Collaboration πŸ‘¬

One of the most compelling reasons to use Storybook is that it enhances team collaboration. Whether you're a developer, a designer, or a product manager, Storybook offers something for everyone.

2. UI Version Control πŸ›‚

Just like how Git helps you manage versions of your code, Storybook helps you manage versions of your UI components. This is particularly helpful in larger teams where multiple people may be working on the UI simultaneously.

3. Embed Stories πŸ“œ

Storybook allows embedding component stories into wikis, Markdown docs, and Figma. This provides an interactive preview of components directly within documentation. The embedded stories stay synced as components evolve. This helps document and demonstrate components in an interactive way.

4. Rapid Prototyping 🎨

Storybook allows you to quickly create prototypes by combining various components. This is beneficial not just for developers but also for designers and product managers who can see how different components interact with each other without having to wait for a full-fledged application build.

5. Autodocs πŸ“

Storybook Auto-docs generates documentation automatically based on component stories. It captures props tables, usage docs, and code samples without additional effort.

AutoDocs is configured through doc blocks in Storybook's MDX files or in a custom template file for automatic documentation.

Developers write stories once and get comprehensive docs for free. This reduces duplication and keeps docs synchronized. Auto-docs helps produce consistent interactive documentation with less manual work.

⬇️

In addition to Storybook's powerful core features, you can further enhance your workflow by integrating add-ons and third-party tools like Chromatic and Cypress. We’ll look at these add-ons and tools in the next section.

Storybook add-ons πŸ’’

Storybook add-ons are plugins that extend the functionality of Storybook and enhance the developer experience. We'll be using several helpful add-ons including addon-cssprops, addon-essentials, and addon-a11y.

1. addon cssprops πŸ‘©πŸΌβ€πŸŽ¨

The addon-cssprops add-on allows you to view the CSS properties of an element in the Storybook UI. It shows the resolved values of CSS variables and other styles. This helps debug styling issues efficiently. Lets look at two types of CSS variables that can be used with the addon-cssprops

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

In this example, the --ui-custom-button-color is used a fallback to storybook if --ui-button-color doesn't contain anything

you can then use the storybooks file and use the styles using css props with the add-on ⬇️

parameters: {
    cssprops: {
      'ui-custom-button-color': { value: 'rgb(0, 169, 79)', control: 'color', description: 'color of the page' }
    },
  }

The variables aren't added by default but must be explicitly imported. You can have another style sheet that controls individual stories. Here’s the same example using our button story :

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

In the case of a service-side variable, you can pass the --ui-button-color from the service to the storybooks component and it will use this variable as the default styling for the services button background.

ℹ️

This mechanism of controlling css variables lets you freely create a basic structure to your components and promotes an easy way to let you choose the best styling for your components. Once you found the right color, you can extends this basic structure and reuse and change the components styles in any service you might create !

2. addon a11y

Addon a11y enhances your Storybook experience by checking your components for accessibility issues in real-time.

It integrates with the A11y audit library to provide immediate feedback, helping you build more accessible UIs. a11y add-on is a must-have for developers committed to making inclusive applications.

3. addon essentials

The @storybook/addon-essentials package bundles together several commonly used Storybook addons into one easy installation. Specifically, this addon is bundled with :

Together these provide live editing via controls, documentation, layout tools, debugging, and responsive utilities out of the box 😍. It simplifies setup by bundling the most common needs instead of installing each addon separately. Overall, addon-essentials streamlines enriching Storybook with interactive documentation and debugging capabilities πŸ’ͺ.

What is Storybooks Chromatic πŸ”

Chromatic amplifies the functionalities of Storybook by offering a cloud-based platform that specializes in UI testing, visual validations, and collaborative work settings. Its key features enhance the team experience in the following ways.

Storybook and Cypress for End-to-End Testing βŒ›

Cypress brings automated end-to-end testing right into your Storybook environment. This fusion offers a trifecta of features that supercharge your component testing

Chromatic and Storybook work in tandem to enable continuous testing in your development pipeline. Storybook serves as a library for your UI components, while Chromatic automates visual testing by capturing snapshots of those components.

Every code commit triggers these tests, ensuring visual consistency and catching regressions before they reach production, thus achieving a seamless, continuous testing environment.

πŸ‘‡πŸ½

Here are some specific benefits of using Cypress with Storybook in a team setting

Building Components with Storybook and Stencil πŸš€

Creating a component library involves several steps, from setting up your development environment to actually writing your components and making them reusable.

In this section, we'll walk through setting up a component library using Storybook and Stencil, and we'll include the use of CSS props for styling, a11y for accessibility, and some essential Storybook add-ons.

Initialize Stencil Project ⬇️

Install Stencil CLI if not installed and create a new Stencil project :

# install stencil in your local machine
yarn global add stencil

# create a stencil project 
yarn create stencil

⚠️

When creating the project with the CLI, make sure to choose the "component" type when prompted

Creating Atomic Components βš›οΈ

In this project, we'll adopt the Atomic Design Methodology, a structured framework for assembling user interfaces.

Beginning with foundational elements like buttons, termed 'atoms,' we'll layer complexity by creating 'molecules' like cards, and ultimately weave these into 'organisms' that constitute full layouts or pages.

πŸ’‘

This hierarchical, modular strategy significantly improves both the reusability and maintainability of our components. Let's dive into crafting our library

Atom : The ui-button πŸ”˜

Navigate to the src/components folder and create a folder called ui-button and Create a ui-button.tsx file.

This code defines a StencilJS web component for a button (ui-button). The component uses Shadow DOM and has a linked SCSS stylesheet for styling. It accepts a text prop to display on the button. The render method returns the actual button element wrapped in a <Host> element, which represents the component's root. If no text is passed, it defaults to an empty string :

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

@Component({
  tag: 'ui-button',
  styleUrl: 'ui-button.scss',
  shadow: true,
})

export class UiButton {
  @Prop() text: string;

  render() {
    return (<Host><button>{(this.text || '')}</button></Host>);
  }
}

Create a ui-button.scss file for the ui-button and include CSS rules that utilize CSS custom properties (variables). For example ⬇️

:host {
  display: block;
}

button {
  padding: 10px;
  border: 1px solid black;
  border-radius: 30px;
  background-color: var(--ui-custom-button-color, var(--ui-button-color, var(--ui-button-color)));
  color: white;
}

 Molecule : A ui-card βœ‰οΈ

Navigate to the src/components folder, create a folder called ui-card and create a ui-card.tsx file.

Add the code to the file to create a simple card. This defines a Stencil component called ui-card with a also a text prop and shadow DOM :

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

@Component({
  tag: 'ui-card',
  styleUrl: 'ui-card.scss',
  shadow: true,
})

export class UiCard {
  @Prop() text: string;

  render() {
    return (<Host>
      <div class="ui-card">
        <div class="ui-card__header">
					{(this.text || '')}
				</div>
        <div class="ui-card__action">
					<slot name="action" v-bind="scope" />
				</div>
      </div>
    </Host>);
  }
}

Create ui-card.scss file to hold all styling of the card component :

:host {
  display: block;
}

.ui-card {
  background-color: var(--ui-custom-card-color, var(--ui-card-color, var(--ui-card-color)));
  width: fit-content;
  border-radius: 10px;
  padding: 10px;

  &__header {
    color: white;
    padding: 10px;
  }

  &__action {
    padding: 10px 0;
  }
}

Organism : The ui-home 🏑

Proceed to the src/components directory and establish a new folder named ui-home. Inside this folder, crerate a file named ui-home.tsx.

Within this file, paste the codes below to it. This sets up a Stencil component dubbed ui-home, which is equipped with a text property and utilizes a shadow DOM. The component is capable of displaying a header derived from the text property and a designated content slot. Styling for the component is managed through a separate SCSS file, and JSX is employed within the render method.

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

@Component({
  tag: 'ui-home',
  styleUrl: 'ui-home.scss',
  shadow: true,
})

export class UiHome {
  @Prop() text: string;

  render() {
    return (<Host>
      <div class="ui-home">
        <div class="ui-home__header">
          {(this.text || '')}
        </div>
        <div class="ui-home__content">
          <slot name="content" v-bind="scope" />
        </div>
      </div>
    </Host>);
  }
}

Just like our other components, let’s add the styles for the ui-home.scss file ⬇️

:host {
  display: block;
}

.ui-home {
  background: var(--ui-custom-home-color, var(--ui-home-color, var(--ui-home-color)));
  padding: 10px;

  &__header {
    color: white;
  }

  &__content {
    padding: 10px;
  }
}

❇️

That’s it for stencil components. We’re done with our core component creation and setup. Now, let’s set up our Storybook πŸ‘€ !

Installing Storybook πŸͺ„

Navigate to your project root, which in this case is where our Stencil project is. Initialize Yarn 3 and Install Storybook :

# move to your project directory
cd project-name

# install storybook to your project 
yarn dlx sb@next init --type html

Configuring Storybooks Add-ons, Build & Documentation βš™οΈ

Install the required storybook add-ons :

# install all addons needed
yarn add -D @storybook/addon-essentials @storybook/addon-a11y @ljcl/storybook-addon-cssprops

Edit .storybook/main.ts and add the add-ons to the addons array along with the following configurations :

This Storybook configuration file set up includes ⬇️

const path = require("path");

module.exports = {
  stories: ["../src/**/*.stories.mdx", "../src/**/*.stories.@(js|jsx|ts|tsx)"],
  staticDirs: ["../public"],
  typescript: { reactDocgen: "react-docgen", skipBabel: true, check: false },

  addons: [
    "@ljcl/storybook-addon-cssprops",
    "@storybook/addon-a11y",
    "@storybook/addon-essentials"
  ],

  docs: { autodocs: "tag", defaultName: "readme" },

  framework: { name: "@storybook/html-webpack5", options: {} },

  webpackFinal: async (config, { configType }) => {
    config.resolve.alias = {
      ...config.resolve.alias,
      "@": path.resolve(__dirname, "../src"),
    };

    config.module.rules.push({
      test: /\.(ts|tsx)$/,
      loader: require.resolve("babel-loader"),
    });

    config.resolve.extensions.push(".ts", ".tsx");
    return config;
  },
};

Configuring Storybook with preview.tsx

The preview file is an essential element in the Storybook setup. The preview.tsx file below sets default parameters for storybook like backgrounds, controls, hierarchy format, story sorting, and table of contents options for documentation.


export const parameters = {
  actions: { argTypesRegex: "^on[A-Z].*" },
  backgrounds: { default: 'light' },

  controls: {
    expanded: true,
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },

  options: {
    hierarchySeparator: /\//,
    hierarchyRootSeparator: '|',
    storySort: {
      method: 'alphabetical',
      order: [ 'intro', [ 'organism', 'molecule', 'atom' ],
      locales: 'en-US',
    },
  },

  docs: {
    toc: {
      headingSelector: 'h1, h2, h3',
      ignoreSelector: '#primary',
      title: 'table of contents',
      disable: false,
      unsafeTocbotOptions: {
        orderedList: false,
      },
    },
  },
}

Adding Stories for your Stencil Components 🌟

Create a ui-button.stories.ts inside : src/stories/atom/ui-button

The code below defines a Storybook story for the ui-button UI component. The story sets up metadata, such as a title, arguments with controls, and a template that renders the component using the arguments. The story also exports the story, providing an example usage for the ui-button documentation.

import { h } from 'jsx-dom';
import { action } from '@storybook/addon-actions';

export default {
  title: 'stories/atom/UiButton',
  tags: ['autodocs'],
  argTypes: {
    text: {
      control: 'text',
      description: 'text of the button',
      type: { summary: 'string' },
      defaultValue: { summary: 'select' },
    }
  },
  args: {
    text: 'select',
  },
  parameters: {
    cssprops: {
      'ui-custom-button-color': { value: 'rgb(0, 169, 79)', control: 'color', description: 'color of the page' }
    },
  },
};

const Template = (args) => (
  <ui-button text={args.text} onClick={() => action('button-select')(args.text)} />
);

export const UiButton = Template.bind({});

Similarly, create ui-card.stories.ts inside : src/stories/molecule/ui-card

The ui-card Storybook story defines how to render the ui-card UI component using JSX syntax. It also specifies controls and default values that can be tweaked by the Storybook interface, such as colors and text options. The story also includes an action for the button within the card to log events, respecting Atomic Design Principles.

import { h } from "jsx-dom";
import { action } from "@storybook/addon-actions";

export default {
  title: "stories/molecule/UiCard",
  tags: ["autodocs"],
  argTypes: {
    text: {
      control: "text",
      description: "text of the card",
      type: { summary: "string" },
      defaultValue: { summary: "card" },
    },
  },
  args: {
    text: "card",
  },
  parameters: {
    cssprops: {
      "ui-custom-card-color": {
        value: "rgb(109, 118, 178)",
        control: "color",
        description: "color of the card",
      },
    },
  },
};

const Template = (args) => (
  <ui-card text={args.text}>
    <ui-button
      slot="action"
      text="select"
      onClick={() => action("button-select")(args.text)}
    />
  </ui-card>
);

export const UiCard = Template.bind({});

Finally create ui-home.stories.ts inside : src/stories/organism/ui-home

Create a Storybook story for the ui-home component with the code below. It imports JSX and Storybook add-ons, defines Storybook meta like title and args, and shows how args are passed to the ui-home component. The Template renders the component `with slots and events. It exports UiHomeStory bound to the Template. Since the Component is an organism based component, it will include the atom and molecule components we created above :

import { h } from "jsx-dom";
import { action } from "@storybook/addon-actions";

export default {
  title: "stories/organism/UiHome",
  tags: ["autodocs"],
  argTypes: { text: { type: { summary: "string" }, description: "text of the header", defaultValue: { summary: "header text" }, control: "text", }, },
  args: { text: "header text", },
  parameters: { cssprops: { "ui-custom-home-color": { value: "rgb(224, 224, 224)", control: "color", description: "color of the page", }, }, },
};

const Template = (args) => (
  <ui-home text={args.text}>
    <ui-card slot="content" text="card text">
      <ui-button slot="action" text="select" onClick={() => action("button-select")(args.text)} />
    </ui-card>
  </ui-home>
);

export const UiHome = Template.bind({});

❇️

In the next section, we’ll take a look on how we can utilize StencilJS to provide multi-framework support to export your components into a service of your choice.

Exporting The Library with Multi-Framework Support πŸ¦‘

The StencilJS project can be configured to export components for multiple frontend frameworks like Vue, Angular, and React. This is a powerful feature that enables reusable web components across a diverse set of technologies !

The Configuration File πŸ› οΈ

The main configuration file for a StencilJS project is usually called stencil.config.ts. Within this file, you can specify various output targets to export your components πŸ‘‡πŸΌ

// stencil.config.ts

import { Config } from "@stencil/core";
import { sass } from "@stencil/sass";
import { vueOutputTarget } from "@stencil/vue-output-target";
import { angularOutputTarget } from "@stencil/angular-output-target";
import { reactOutputTarget } from "@stencil/react-output-target";

export const config: Config = {

  namespace: 'storybook-stencil',
  outputTargets: [

		// generates a dist directory with ECMAScript modules.
    { type: "dist", esmLoaderPath: "../loader" },

		// creates custom elements in the dist directory.
    { type: "dist-custom-elements" },

		// generates README documentation.
    { type: "docs-readme" },

    // builds a www directory but without service workers.
    { type: "www", serviceWorker: null },

    // exports components as Vue components.
    vueOutputTarget({
      componentCorePackage: "storybook-stencil",
      proxiesFile: "./lib/vue/components.ts",
    }),

		// exports components as Angular components and directives.
    angularOutputTarget({
      componentCorePackage: "storybook-stencil",
      directivesProxyFile: "./lib/angular/components.ts",
      directivesArrayFile: "./lib/angular/directives.ts",
    }),

		// exports components as React components.
    reactOutputTarget({
      componentCorePackage: "xotoboil-storybook-stencil",
      proxiesFile: "./lib/react/index.ts",
      includeDefineCustomElements: true,
    }),
  ],

	// adds scss accessible by all components.
  plugins: [
    sass({
      injectGlobalPaths: ["src/styles/index.scss"],
    }),
  ],
};

This Stencil config exports components for multiple frameworks. It generates ESM, custom elements, docs, and the www builds. It passes options like the component package names, proxies file, directives file and allows the compiled components to be imported and used across Vue, React, Angular.

The key outputs are :

The Output Directory : dist πŸ“¦

After building the project with yarn build, a dist directory will be created, containing all the export targets :

dist/
β”œβ”€β”€ custom-elements
β”œβ”€β”€ loader
β”œβ”€β”€ storybook-stencil.js
β”œβ”€β”€ collection
└── types

Preview Your Components πŸƒπŸ½β€β™‚οΈ

# run your storybook library
yarn storybook

🎈

That’s it! Click on your stories to view them and tweak the component properties using the controls panel

In Conclusion πŸ”₯

Storybook and Stencil are powerful tools for building reusable component libraries. By leveraging Stencil for framework-agnostic web components and Storybook for visual documentation and testing, developers can create consistent, declarative UI components that are easy to share across projects.

With the techniques covered in this guide, from generating components, to configuring stories and add-ons, you have the fundamentals to start building a robust component library for your team or organization!

As your library grows, Storybook will become an invaluable hub for designers, testers, and engineers to collaborate and ensure your UI components look, function, and document exactly as intended.