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.
- Developers : It allows them to work on one component at a time without worrying about the entire application's state. This modularity makes it easier to debug and test components.
- Designers : They can easily review how different components look and feel, making it easier to maintain design consistency across the application.
- Product Managers : They can see all the components in one place, which makes it easier to understand what is available and how it can be combined to create new features.
- Testers: Storybook provides a unified platform where testers can easily interact with individual components in isolation, making it simpler to conduct unit and integration tests.
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
- πͺ Local variables : These are defined in a Storybooks story and are available to that story by default. For example, you may define default font sizes, colors, etc :
.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' }
},
}
- πͺ Service-side variables : These are imported into a specific story from an external CSS variables library through css variables. This allows sharing common variables across different components and stories.
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 :
- @storybook/addon-actions : Logs actions like clicks, hovers, keypresses to the actions panel for debugging. Automatically infers action names from event handlers.
- @storybook/addon-backgrounds : Allows setting backgrounds on the preview to style components in different states. Supports gradients.
- @storybook/addon-controls : Automates prop controls generation from component args/props. Supports types like string, number, boolean, object.
- @storybook/addon-docs : Generates documentation pages from MDX, CSF, or automatically from components.
- @storybook/addon-toolbars : Adds extra toolbars like zoom, grid layout, fullscreen.
- @storybook/addon-viewport : Responsive testing with a toolbar to set viewport sizes.
- @storybook/addon-measure : Inspect size and position of elements.
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.
- Effortless Visual Reviews : With Chromatic, designers and product managers can scrutinize UI changes directly in the web browser, eliminating the need to pull source code or set up the application. This accelerates the feedback loop and simplifies the approval process.
- Automated Visual Consistency Checks : Developers can take advantage of Chromatic's visual regression tests to ensure that any new modifications don't inadvertently affect existing components. This helps in maintaining a consistent and reliable UI.
- Easy Sharing for Remote Teams : Chromatic makes the updated UI components accessible to the team via a simple shared link, making it an invaluable tool for distributed teams.
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
- Interaction Testing : Simulate real-world user interactions within Storybook, like clicks or form submissions, to ensure components behave as expected.
- Accessibility Checks : Run automated tests to confirm your components meet accessibility standards, making your application more inclusive.
- Snapshot Verifications : Capture and compare component states to identify visual changes, streamlining the QA process for visual regression 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
- Streamlined Development : Cypress and Storybook together streamline the development process, making it easier for teams to collaborate.
- Automated Testing : Cypress provides automated end-to-end testing, ensuring each component functions as intended.
- Component Isolation : Storybook allows for isolated component development and testing, making it easier to spot issues.
- Real-Time Testing : The integration enables real-time testing, allowing for immediate feedback and quicker iterations.
- Quality Assurance : This combination improves code quality and ensures a consistent user experience, reducing the risk of bugs making it to production.
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 β¬οΈ
- A stories array to declare where to find the stories in the project.
- A static directory array to declare public directories.
- A typescript object to add typescript based configurations.
- An add-ons array to include CSS props, A11y and essentials.
- A docs object to allow MDX/TSX.
- WebPack configurations for aliases.
- Babel Loaders to allow TSX.
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 :
- Vue components via
vueOutputTarget
- Angular components/directives via
angularOutputTarget
- React components via
reactOutputTarget
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.