Scaffolding a React Component Library with Storybook (Using TypeScript)

When a company develops and releases a new product onto its platform, users expect this product to deliver an experience similar to another product (on the platform) they have worked with. For example, many people, including yourself, are probably familiar with at least one of Google's collaborative office tools, such as Google Sheets and/or Google Docs, that integrate seamlessly with Google Drive. Now, suppose Google announces a new collaborative office tool at Google I/O. If you decide to use this tool, then you may notice how much faster it takes for you to learn this tool, along with its shortcuts and tricks, because the interface contains features that you have previously interacted with in other tools. Across each tool, the appearance of these features, such as the editing toolbar and sharing dialog, remains consistent since they draw upon the same set of foundational elements, controls, colors, typography, animations, etc.

By building a component library, we can centralize all reusable components at one location and access these components from any of our products. Furthermore, pairing a component library with a design system unifies every product within the platform under a singular brand identity. For distributed teams that work on products independently of one another, this allows teams to follow the same design principles/philosophies/patterns, share code and create components in isolation. Writing components in an environment outside of our application makes them flexible and adaptable to any specific layout requirements. This way, the component's design can account for both known and unforeseen use cases. Anytime designers, developers and product managers contribute to the library and update the components, those changes immediately propagate down to the products using those components.

With so many different stakeholders involved, we need to build durable components that are thoroughly tested and well-documented. A popular open-source tool for organizing and building components in isolation is Storybook, which comes with a sandbox for previewing/demoing components and mocking use cases to capture different states of a component in stories. Documenting these use cases as stories especially helps in onboarding new team members. Storybook has integrations with different front-end libraries/frameworks: React, Vue, Angular, Svelte, etc. Additionally, if you need functionality that Storybook doesn't already provide, then you can create addons to extend Storybook.

Below, I'm going to show you how to add Storybook to your component library.

Installation and Setup#

To get started, clone the following repository:

This repository contains a component library with customizable D3 visualizations written in React and TypeScript. Currently, this repository only has one visualization component, a scatterplot.

Inside of the project directory, install the dependencies:

For this tutorial, we will be adding Storybook to this library and writing stories for its scatterplot component.

Getting Started with Storybook#

Add Storybook to the library with Storybook CLI. As of Storybook v6.0, Storybook CLI automatically detects whether the project is TypeScript-based and configures Storybook to support TypeScript without any additional configuration.

This command creates the following directories and files within the project:

  • .storybook - Contains Storybook configuration files (main.js and preview.js).

    • main.js - The main configuration file. It tells Storybook where stories are located (stories, which is set to an array of globs for pattern-matching stories' filenames, relative to the .storybook directory) and the addons to supercharge Storybook (addons, which is set to an array of addons). Specify the webpackFinal and babel options to customize the webpack and babel configurations respectively.

    • preview.js - The "preview" iframe configuration file. It tells Storybook how to render stories within the "preview" iframe, which renders components in isolation. By default, only one named export is specified in preview.js: parameters. Parameters define the metadata of a story, such as its background color. This metadata can either be defined within a story to target an individual component or within the preview.js file via the parameters named export to globally target all components. There are two other named parameters, decorators and loaders (experimental), for mocking context data and loading asynchronous data into stories respectively.

  • src/stories - Contains Storybook stories. Initially, this directory contains three example stories for the basic components <Button />, < Header /> and <Page />.

Within package.json, several Storybook dependencies are now listed under devDependencies.

Along with these new dependencies, several NPM scripts are added for running Storybook locally and building Storybook as a static web application (to host on a cloud service and publish online).

Run the storybook NPM script to run Storybook locally.

This command spins up Storybook on localhost:6006 and automatically opens Storybook inside the browser. Once Storybook loads, you will be presented an introductory page that contains links to additional learning resources.

Note: You can modify this page by editing the src/stories/Introduction.stories.mdx file.

Anatomy of a Story#

Each *.stories.tsx file defines a component's stories. To view a component's stories in Storybook, click on the item in the left sidebar that corresponds to the component to expand a list of its stories. For example, if you click on the "Button" item, then the canvas displays the first story listed for the <Button /> component. Rendered as an iframe, the canvas allows components to be tested in isolation. Altogether, four stories appear beneath the "Button" item in the sidebar:

  • Primary - Renders the button to indicate a primary action, such as a call to action, to users. Often, a primary button's background color is the brand's color or a hue of blue.

  • Secondary - Render the button to indicate a secondary action, such as closing a dialog, to users. Often, a secondary button's background color is transparent or a hue of gray.

  • Large - Renders a large version of the button.

  • Small - Renders a small version of the button.

Each story describes how certain parameters affect the rendering of the component. To understand what this means, let's look inside the <Button /> component's source and story files.

(src/stories/Button.tsx)

(src/stories/Button.stories.tsx)

Template is a function that accepts args and uses them to render the component. It serves as a template for defining a story. To keep the example simple, args represents props that are passed directly to the component, but they can be modified inside of the Template function for more complex examples. Each story makes a new copy of this template via Template.bind({}) to set its own properties. To specify a story's args, define an args property on the story's copy of the template function, and assign this property an object with the values needed for rendering the component.

For example, the story named Primary renders the <Button /> component with the props { primary: true, label: "Button" } (passed directly to the component within the story's Template function via args). This adds the storybook-button--primary CSS class to the <button /> element and sets its text to "Button."

If you want to experiment with different prop values, then adjust the props within the "Controls" panel below the canvas. Only props of primitive types, such as booleans and strings, are dynamically editable. When you enter "red" into the backgroundColor input field, the button's background color changes to red.

If you switch from "Controls" to "Actions," then you can see logs of event handlers executed as a result of user interactions. For example, the <Button /> component receives an onClick prop that attaches to its <button /> element. When you click the button in Storybook, the panel will print information about that onClick event.

Everything mentioned above also applies to the stories Secondary, Large and Small.

If you press the "Docs" tab, then Storybook shows information about the <Button /> component, such as prop descriptions, the code required to render the component shown in a story, etc.

The prop descriptions come from inline comments written in the props' exported TypeScript interface.

Writing a Story for the <Scatterplot /> Component#

First, let's remove the example stories created during the initialization process.

Next, let's recreate the src/stories/Introduction.stories.mdx file with the contents of the library's README.md file.

(src/stories/Introduction.stories.mdx)

@storybook/addon-docs/blocks provides the building blocks for writing documentation pages. For now, the introductory page will have a webpage title of "Example/Introduction" and will render the extracted contents of the README file to both "Canvas" and "Docs."

Now, let's write some stories for our library's <Scatterplot /> component. Create a src/stories/Scatterplot.stories.tsx file.

Inside of this file, add stories to reflect the following basic use cases for the <Scatterplot /> component:

  • Default - Show a standard scatterplot.

  • Empty - Show an empty scatterplot.

  • Legend - Show a standard scatterplot with colored dots and a legend to map these colors to categories.

For all of the stories to access data fetched from a remote source, we must set a global loader, which runs before the rendering of the stories, inside of the .storybook/preview.js file.

(.storybook/preview.js)

Here, scatterplotData contains the fetched and processed Iris data, which will be available to the stories of the <Scatterplot /> component.

scatterplotData can be accessed by any story template in the project via the template's second argument, the story context, which has a loaded property for accessing loader data.

Back to the src/stories/Scatterplot.stories.tsx file, import the Story and Meta types from @storybook/react and export an object with metadata about the component. The pages of the component's stories will be prefixed with the webpage title "Example/Scatterplot."

(src/stories/Scatterplot.stories.tsx)

Define an object (BASE_ARGS) with template arguments shared by all of the stories. In this case, each story's scatterplot will have the same dimensions (dimensions) and render with the same axes' labels and data (labels, xAccessorKey and yAccessorKey).

(src/stories/Scatterplot.stories.tsx)

Write a template function that renders the <Scatterplot /> component based on args set for each story. Since the default value of data is an empty array, we must explicitly check for a flag isEmpty, which will notify the template function to use this empty array only for the "Empty" story. For the other stories, use the scatterplot data fetched by the global loader.

(src/stories/Scatterplot.stories.tsx)

Note: loaded is undefined when accessing the component's docspage. Unfortunately, all of the inline-rendered stories will be empty because loaders are experimental and not yet compatible with inline-rendered stories in Storybook Docs.

Write the stories. For the "Default" story, just use the base arguments. For the "Empty" story, make sure to notify the template function to use the default empty data array. For the "Legend" story, set two additional fields to args: one for categoryKey (a key to access a record's category) and another for categoryColors (a list of colors to visually differentiate categories).

(src/stories/Scatterplot.stories.tsx)

Altogether...

(src/stories/Scatterplot.stories.tsx)

Default Story

Empty Story

Legend Story

<Scatterplot /> Component's DocsPage

For a final version of this tutorial, check out the GitHub repository here.

Next Steps#

Try integrating Storybook into your own component library!

Sources#