Matthew Pfister
Posted on February 26, 2021
It's the classic story. You've been told that the other teams need a new date picker, drop-down, or styled input. Management is turning on all the sirens, and pressing hard on the platform devs to get something rolled out. The devs, the beasts they are, deliver. However, getting there is a headache. The devs had no idea where in the app these things were going to be used, so they spun up a temporary page to insert and test the components. It worked, but made the dev team say ...
There is, it's called Storybook.
Storybook is a tool to develop component libraries in isolation from the app they will be consumed in. It is essentially a component catalogue that makes it easier for designers and developers to work together to meet the needs of an ever changing application landscape. No need to have the dev build a temporary page to demo the component, Storybook provides tools out of the box to accomplish this. Mainly it provides a nice server that compiles a component library in to an accessible UI for devs to manually test their creations. You can even deploy it to an environment for upper management to play around with. Alleviating the churn of figuring out how the component is going to be showcased or documented.
Where to start?
Organization is usually a great place to start, but keep in mind that everybody puts their ducks in a row differently. In my opinion keeping the storybook modules in the same directory as the component makes the most sense. As a dev, it is nice having the code for the component easily accessible when exploring a new Storybook catalog. Having to jump around the folder structure to find where either the story or component is, is not a fun time.
Naming
Getting a naming convention down would be the next step in creating a nice setup. Naming things is one of the hardest jobs for a dev. However, I think the KISS (Keep It Simple Stupid) principle will help us out here. Just use the following formula [component name].stories.tsx
. This way at quick glance it is abundantly clear what the story is in reference to.
More importantly, splitting out each story in to its own module can clutter up a repository real fast. This is due to the fact that, ore often than not, a component will have several different states that it can render. Meaning that it could have a plethora of different stories created for it to demonstrate this. Opening a folder and seeing fifteen different files isn't the best experience.
That is why it's better to keep all the stories in the same file. Normally, there will only be three to four stories in a given module, which means things won't get out of hand. However, there are always exceptions to the rule, and I've seen storybook modules that get to 500 lines long of just code, no documentation. Don't fret if you see this. Instead take a step back and think about the component you are writing these stories for, Is it too complex? Normally, with a file that long, the answer is yes. So instead of reworking the stories, rework the component itself and then go back to fixing up the stories.
What are stories?
At their core, stories are broken up in to two types: (1) a playground where users can manipulate the component to see what it is capable of, and (2) a series of important states that the component could possibly render on screen. We say important, because we don’t need to show every variation of the component via a story. Just the ones that show off its behavior.
For example, let's say we are building a flashcard for a web based SAT study guide. This would most likely have several different states that are highly likely to occur when used: default, flipped, disabled, and adding indicators to denote which side you are looking at. Since these are all based on different properties we can separate the stories out by them, but that doesn't mean all the properties should be showcased. For instance, className
can be overwritten, but that doesn't display any unique behavior with the component. That simply provides a way for others to overwrite the classes used to style it. It explains itself, and thus doesn't need to be its own story.
To put it simply we want component behaviors that document the unique properties of our component. Anything that is default web behavior is obvious through its usage, and can be left out.
Getting the Environment Setup
Before we dive into some code I should note that I'll be referencing the Flashcard example I mentioned above to demonstrate how to use storybook. You can clone the code here!
Disclaimer: I will not be going over setup for webpack, typescript, babel, or react. Please refer to other guides to get ramped up on them.
Installing the Dependencies
First and foremost you'll need to install all the dependencies necessary to use storybook. Navigate to the root directory of the cloned project and run the following command:
yarn
If you are not following along in the repository I provided, run the following command: yarn add -dev @storybook/react @storybook/components @storybook/addons @storybook/addon-essentials
Configure Storybook
Next we'll need to configure storybook to run correctly. When you run storybook it will look for a directory in the root folder called .storybook
. This directory will house storybook's configuration
file called main.js
and the environment setup file called preview.js
.
// .storybook/main.js
module.exports = {
stories: ['../src/**/*.stories.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: ['@storybook/addon-essentials'],
typescript: {
check: false,
checkOptions: {},
},
};
The above is the code that configures storybook. The stories
property will let storybook know where and what to look for when compiling our stories. The addons
field gives us an easy way to add in plugins for storybook. In our case our only addon is @storybook/addon-essentials, which provides us with a bunch of super nice addons. For instance, it gives us docs
addon for free, which creates a documentation tab for each of our components. This tab provides detailed explanations for each prop, and nicely previews every state of the component on page.
If you want to add some nice markdown into the mix you can install mdx via the packages @mdx-js/react and @types/mdx-js__react. This library will allow you to write markdown in the docs tab. I won't be going over it in this post, but if you feel the urge to add markdown to the docs take a look here
The last field typescript
tells Storybook whether or not we want it to use typescript plugins to generate documentation and other fancy things. By setting the check to false
and giving it an empty options object we are turning off these typescript plugins.
// .storybook/preview.js
import React from 'react';
import { CssBaseline } from '@material-ui/core';
export const decorators = [
(Story) => (
<>
<CssBaseline />
<Story />
</>
),
];
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
};
The preview module is just a way for us to setup the theme, layout, and global environment for our storybook server. There are plenty of addons that you can add in here as well. For instance the withDesigns
hook can be used from the designs addon to showcase a designer's original comps from web-apps such as figma.
Creating our first StoryBook
Finally! We're here. The main event. Creating our very first storybook. I'm not going to paste the entire file here since that would be too much information to digest at one time. Instead I'll go through sections of the code from the file src/Flashcard/Flashcard.stories.tsx
, and explaining what each portion means.
If you are not following along with repo I provided, feel free to copy and paste the following snippets into your own file.
Imports
// src/Flashcard/Flashcard.stories.tsx (Lines 1:4)
import React from 'react';
import { Story } from '@storybook/react';
import Flashcard, { FlashcardProps } from './Flashcard';
The imports are pretty straightforward. We’ll need React of course, since we will be using jsx
. Story
is a type that we’ll need in order to gain the benefit of TypeScript's types and some automagic that storybook does for us to document the props. Lastly, we import the component and its prop types.
Playground Story
Next we’ll start writing out our playground story. To do so we’ll need to create a template for it.
// src/Flashcard/Flashcard.stories.tsx (Line 6)
const Template: Story<FlashcardProps> = (props: FlashcardProps) => <Flashcard {...props} />;
This creates a storybook story identifying that the props passed in by storybook will follow the FlashcardProps
types. Doing this allows storybook to compile a list of controls that can be used in the playground for users to manipulate and update the component within the canvas.
// src/Flashcard/Flashcard.stories.tsx (Line 8)
export const Playground = Template.bind({});
Here we are binding those props to the actual template ultimately creating our very first playground and story! 🎉
Arguments and Types
Now that we've created the playground let's setup the default values for the props. By doing so we are telling storybook what to render, and what we want to be manipulatable inside Storybooks UI. These are known as controls.
// src/Flashcard/Flashcard.stories.tsx (Lines 10:19)
Playground.args = {
Back: 'An open source tool for developing UI components in isolation. It makes building stunning UIs organized and efficient.',
BackSideProps: { elevation: 1, variant: 'elevation', square: true },
Front: 'What is storybook?',
FrontSideProps: { elevation: 1, variant: 'elevation', square: true },
disabled: false,
showBackSideAdornment: true,
showFrontSideAdornment: false,
startFlipped: false,
};
Don't let the field args throw you off, we are setting the props here. These will be bound to the template and passed into the component. Changing these inside the code will always change the initial rendered state of the component inside the storybook UI. However, there is no real need to do that in the code since you can change them through the controls that storybook builds out for you.
Now let's add a little pizzazz to our catalogue, and give some description to each parameter. That way, new devs looking through our component library will know what props to really worry about.
These won't show up in the canvas view. These are rendered in the docs section of the UI instead.
// src/Flashcard/Flashcard.stories.tsx (Lines 21:42)
Playground.argTypes = {
Back: { description: 'Content to be rendered on the back side of the flashcard.' },
BackSideProps: {
description: `These are the properties passed to the back side paper component.<br/><br/>
**elevation:** will change the shadow depth, corresponds to dp. It accepts values between 0 and 24 inclusive..<br/>
**variant:** will change the rendered style of the paper component. Accepts elevation or outlined.<br/>
**square:** if true rounded corners are removed.<br/>
[See the material ui paper docs](https://material-ui.com/components/paper)`,
},
Front: { description: 'Content to be rendered on the front side of the flashcard.' },
FrontSideProps: {
description: `These are the properties passed to the front side paper component.<br/><br/>
**elevation:** will change the shadow depth, corresponds to dp. It accepts values between 0 and 24 inclusive..<br/>
**variant:** will change the rendered style of the paper component. Accepts elevation or outlined.<br/>
**square:** if true rounded corners are removed.<br/>
[See the material ui paper docs](https://material-ui.com/components/paper)`,
},
disabled: { description: 'If set to true the cards flipping functionality will be disabled.' },
showBackSideAdornment: { description: 'Show an adornment to indicate the user is looking at the back side.' },
showFrontSideAdornment: { description: 'Show an adornment to indicate the user is looking at the front side.' },
startFlipped: { description: 'If set to true the card will be rendered back side up.' },
};
Default Story
Now that we have the playground setup we’ll need to show off different states that the component can be rendered in. To start us off we’ll create a default story, which reflects what the component renders when only the required props are passed in.
// src/Flashcard/Flashcard.stories.tsx (Line 44)
export const DefaultStory: Story<FlashcardProps> = () => <Flashcard Back="Side B" Front="Side A" />;
Notice that there are no props being passed into the component from storybook. That’s because we don’t want the user to be able to manipulate the component through controls.
Looking closer at the code you'll notice that we export the default state as DefaultStory
. That camel casing doesn't look too nice. Let's fix that by renaming it.
// src/Flashcard/Flashcard.stories.tsx (Line 46)
DefaultStory.storyName = 'Default';
Setting the field storyName
will make sure that the default state of the component is found under the sidebar item 'Default'.
Additional Stories
Some components have states that we want to highlight via storybook. This can be accomplished by creating additional story components. For the Flashcard component we have five other states that we need to highlight:
- Starts flipped, or in other words, on the back side.
- Is disabled, or in other words, not flippable.
- Has only the front side adornment.
- Has adornments on both sides.
- Has no adornments.
The following code snippets cover these different states.
// src/Flashcard/Flashcard.stories.tsx (Lines 48:50)
// State: Starts flipped, or on the back side.
export const FlippedStory: Story<FlashcardProps> = () => <Flashcard Back="Side B" Front="Side A" startFlipped={true} />;
FlippedStory.storyName = 'Flipped';
// src/Flashcard/Flashcard.stories.tsx (Lines 52:54)
// State: Is disabled, or not flippable.
export const DisabledStory: Story<FlashcardProps> = () => <Flashcard Back="Side B" Front="Side A" disabled={true} />;
DisabledStory.storyName = 'Disabled';
// src/Flashcard/Flashcard.stories.tsx (Lines 56:60)
// State: Has only the front side adornment.
export const FrontSideAdornmentStory: Story<FlashcardProps> = () => (
<Flashcard Back="Side B" Front="Side A" showFrontSideAdornment={true} showBackSideAdornment={false} />
);
FrontSideAdornmentStory.storyName = 'Front Side Adornment';
// src/Flashcard/Flashcard.stories.tsx (Lines 62:66)
// State: Has adornments on both sides.
export const DoubleAdornment: Story<FlashcardProps> = () => (
<Flashcard Back="Side B" Front="Side A" showFrontSideAdornment={true} />
);
DoubleAdornment.storyName = 'Double Adornment';
// src/Flashcard/Flashcard.stories.tsx (Lines 68:72)
// State: Has no adornments.
export const NoAdornment: Story<FlashcardProps> = () => (
<Flashcard Back="Side B" Front="Side A" showBackSideAdornment={false} />
);
NoAdornment.storyName = 'No Adornment';
Lastly, we'll need to have a default export to give storybook the general configuration for this component's stories. Like so:
// src/Flashcard/Flashcard.stories.tsx (Lines 74:77)
export default {
title: 'Flashcard',
component: DefaultStory,
};
In this exported configuration object, the title field is very important. It determines where the stories show up in the storybook navigation hierarchy, and follows a strict naming convention. Use one of the following:
- [Component Name]
- [Category Name]/[Component Name]
- [Component Name]/[Sub-Component Name]
- [Category Name]/[Component Name]/[Sub-Component Name]
I'll go more into what each of these mean below, when we actually run the server.
The second field passed into the default export is the first story we want to render after the playground. In most cases you want to render the default story.
Running the Server
We've written the code, now let's see it in action! You can now successfully run storybook by running the following command:
yarn start
If you are not following along in the repository I provided, run the following command: start-storybook -p 6006
The command should open up your default browser and navigate you to the right page in a new tab. However if it doesn't for some reason, open up your favorite browser and navigate to localhost:6006
.
When the page loads you should see the following:
This is the landing page. On the left you have the sidebar that nicely nests your components for you to easily navigate through. On the right you'll see the canvas, which is where the components are rendered. The playground should be selected by default. Lastly, at the bottom of the page, you should see all the controls for you to play with. Changing these will update the component in the playground.
The Sidebar
Taking a closer look at the sidebar you can see the breakdown of all our stories. Looking back at the options listed above, you can see we used the first option. All the stories we created will be nested under the Component name, like so:
If you follow the second option and do something like 'Layout/Flashcard' you would get something like this:
The last two options are a way to show pieces of a more complex component. For instance, Material-UI's Card component has sub-components header, content, and actions which is a great example of using those last two options since you want those sub components nested under the Card component.
The Canvas
The canvas is where the component is rendered.
Switching between the stories in the sidebar will render different states of the component.
If you try switching to another story now, you'll notice that the controls section becomes blank. This is by design since we don't want to show controls for non-playground stories. Remember manipulating the component should only be done in the playground.
Viewport Tabs
At the top of the canvas you'll notice that there are a series of buttons. The ones on the far left are tabs to switch between viewports. You should see Canvas and Docs like so:
Clicking on the Docs tab will switch the viewport to display the documentation for the component you are currently looking at like so:
This page shows the descriptions we wrote for the props. More importantly, it allows us to look at the code for all the stories. As you can see every story is rendered onto this page for ease of access. Navigation via the sidebar will scroll the viewport to the selected state.
Zooming
The buttons next to the viewport tabs are all for zooming, which is self explanatory.
The first one zooms in, the second one zooms out, and the third one resets the zoom to the default.
Viewport Options
The buttons after the zooming functionality are nice-to-haves.
The first button is to change the background color from light to dark. This is useful if your component has lighter or darker styling. It's a way for it to really stand out for analyzation. The second button adds a grid to the viewport. This is to help align with the spacing and seizing of the design, for instance, from a designer's comps. The last option is to change the viewport size to other media sizes. Mobile, and tablet are but a few options. This is to test the responsiveness of the component.
Controls
At the bottom of the screen, when canvas is selected, are the controls. It should look like:
This area is where the user can manipulate the data that is passed into our playground story. When you bind the template and playground story the arguments that you add to the args field on the playground will be compiled into different inputs based on their inherent type. So a boolean will be converted into a switch, strings will be converted into text fields, and so on. Storybook does all the heavy lifting for you, so long as you provide the args field on the playground story.
Goodbye for Now!
That, my fellow dev, is Storybook in a nutshell. There are of course more in-depth topics that I didn't go over in this article, but the all the fundamentals of getting up and running is here.
I am currently looking into making a part 2 for this article covering an mdx
version of the Flashcard stories. However, I can't make promises as to when that will get out, or if it will get done. Let me know if that is something that would be interesting for you to read about in the comments below.
Thanks for reading! I hope I didn't put you to sleep. If you have any questions please feel free to ask them in the comments below as well.
Cheers! 🍺🍺
Posted on February 26, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.