Introduction
Modern client-side architecture demands the need to create efficient user interfaces (UI) for end-users.
Products need to be adopted by different categories of users based on screen sizes, accessibility, and device types. Thus, users demand different appearances and behaviour for a single component.
Storybook helps to solve this challenge by enabling developers to build an agreed-upon component libraries that are parts of a design system.
In this guide, you will:
- Learn how a design system can be beneficial to your team's Next.js application.
- Build and document a component library using Storybook.
- Deploy your design system to enhance collaboration between the developers and the designers in your team.
What is a Design System?
A design system is a set of standardised design principles and guidelines that allows users to have a consistent experience with a product. It is an approach to building UI components.
A design system consists of design resources, including typography, colour, icons, forms, buttons, layouts, patterns, and components.
These resources are reusable, documented, can be tested, and do not need to be rebuilt from scratch when needed.
The Strapi Design System is an example of a design system.
Benefits of a Design System
- It enables the engineering team to focus on building the application logic without having to worry about design decisions.
- A deployed design system enhances communication and collaboration between designers and engineers. Thus, reducing workflow friction.
- A company that has different products can maintain brand consistency and alignment when teams share a design system.
- A well-documented design system helps to improve developer onboarding without the need for newer team members to feel confused with their tasks.
Getting Started with Storybook
Storybook is a UI development tool that helps to build design systems. It allows developers to build, document, test, and deploy isolated UI components.
It offers the ability for developers to view and interact with their components, while the components are not yet rendered in their application. Each component has stories that define the appearance and behaviour of that component.
Later in this article, you will gain hands-on experience with stories. Now, let us move on to how to set up our project.
Project Setup and Installation
Install Next.js
Create an initial Next.js project setup by running the command below on your terminal:
1npx create-next-app@latest
2# or
3yarn create next-app
4# or
5pnpm create next-app
6# or
7bunx create-next-app
Select TypeScript as the programming language so you can follow up with this article.
Install Storybook
Navigate to the root directory of your Next.js project, and install Storybook with the command:
1npm create storybook@latest
2# or
3pnpm create storybook@latest
4# or
5yarn create storybook
6# or
7bunx storybook@latest init
Run your Next.js Application
Run your Next.js application development server with the command:
1npm run dev
2# or
3pnpm dev
4# or
5yarn dev
6# or
7# bun run dev
Run your Storybook UI
To simultaneously view, edit, and interact with your Storybook on a different browser tab alongside your Next.js app, open a separate terminal on your IDE.
Navigate to the root directory of your project and run your Storybook with the command below:
1npm run storybook
2# or
3pnpm run storybook
4# or
5yarn storybook
Configure your Storybook Project
To specify the folder path from which your stories will run, update the .storybook/main.ts|js
configuration file to this:
1// .storybook/main.ts
2
3import type { StorybookConfig } from '@storybook/nextjs';
4
5const config: StorybookConfig = {
6 "stories": [
7 // add the folder path
8 "../app/**/*.mdx",
9 "../app/**/*.stories.@(js|jsx|mjs|ts|tsx)"
10 ],
11 // ...
12 };
13export default config;
This allows Storybook to run all the .stories
or .mdx
files that are located inside the app
directory only. Files that end with .mdx
are created for documentation purposes.
Remove Storybook Example Code
The stories
folder that is located inside the root directory of your project contains the Storybook sample code that can be deleted when not needed in your project.
How to Write Stories in Storybook
Stories are created in a story file that is saved in the same directory as the component. Story files follow the .stories.tsx|jsx
name saving convention.
A story represents the appearance and behaviour of a component. It is an object that describes the props and mock data that are passed into a component.
We will be declaring our default export as meta
. Stories are declared using the UpperCamelCase
letters.
The sample code below demonstrates how to write stories:
1// Sample.stories.tsx
2
3import type { Meta, StoryObj } from "@storybook/nextjs"
4
5import MyComponent from './Components/MyComponent.tsx'
6
7
8type MyComponentMeta = Meta<typeof MyComponent>
9
10const meta = {
11 title: 'ComponentsGroup/ComponentOne',
12 component: MyComponent,
13} satisfies MyComponentMeta
14
15
16export default meta;
17
18type MyStory = StoryObj<typeof meta>
19
20
21export const StoryOne = {} satisfies MyStory
22
23export const StoryTwo = {} satisfies MyStory
24
25export const StoryThree = {} satisfies MyStory
26
27export const StoryFour = {} satisfies MyStory
Since we are writing our project in TypeScript, the default export needs to satisfy the imported component using the Meta
type.
Then, each story needs to satisfy the default export using the StoryObj
type.
Meta
: helps to describe and configure the component.StoryObj
: helps to configure the component's stories.satisfies
: is a strict type checking operator.component
: represents the imported component that will be rendered in the story file.title
: allows you to group multiple components into a single folder on the Storybook UI. The/
separator distinguishes the folder-to-file pathname.
How to Render Stories in Storybook
You can render your stories either by passing the component's props
as args
, or by using the render
function.
1. args
: is an object that contains the values for the props
that can be passed into a component.
Create an Article
component:
1// app/UI/Article/Article.tsx
2
3import React from 'react'
4
5
6export interface ArticleProps {
7 text: string;
8 children: React.ReactNode;
9}
10
11export default function Article({...props}: ArticleProps) {
12
13 const {text, children} = props
14
15 return(
16 <article style={{border: '1px solid black', padding: '20px'}}>
17 <h1>{text}</h1>
18 {children}
19 </article>
20 )
21}
Next, create an Article.stories.tsx
file:
1// app/UI/Article/Article.stories.tsx
2
3import type { Meta, StoryObj } from '@storybook/nextjs'
4import Article from './Article'
5
6
7const meta = {
8 component: Article,
9 args: {
10 text: 'default export',
11 children: (
12 <div>
13 <h3>heading level 3</h3>
14 <p>paragraph.</p>
15 </div>
16 ),
17 },
18} satisfies Meta<typeof Article>
19
20
21export default meta
22
23type Story = StoryObj<typeof meta>
24
25
26export const Default = {
27 args: {
28 ...meta.args
29 }
30} satisfies Story
In the story file above, we passed the text
and children
props for the Article
component into the args
property for the default export. Then, we create a Default
story that inherits the same args
as the default export.
Here, let us create a NewHeading
story with args
that are different from the other story. Update the Article.stories.tsx
file with the code below:
1// app/UI/Article/Article.stories.tsx
2
3
4// ...
5
6export const NewHeading = {
7 args: {
8 text: 'new story',
9 children: (
10 <div>
11 <h3>new heading level 3</h3>
12 <p>new paragraph.</p>
13 </div>
14 ),
15 },
16} satisfies Story
The args
for the NewHeading
story override that of the default export.
Stories can also inherit args
from other stories, using the object spread operator {...}
. For example, we want the args
to inherit from the BehaviourOne
story that has already been created in a SampleComponent.stories.tsx
file:
1// app/UI/Article/Article.stories.tsx
2
3// ...
4import * as SampleStories from '../SampleComponent.stories.tsx'
5
6
7// ...
8
9// inherits all args from the imported story
10export const BehaviourOneDuplicate = {
11 args: {
12 ...SampleStories.BehaviourOne.args
13 }
14}
15
16// inherits only children args from the imported story
17export const ChildrenDuplicateOnly = {
18 args: {
19 text: 'text is not a duplicate',
20 children: ...SampleStories.BehaviourOne.args.children
21 }
22}
2. render
: is a function that accepts args
as parameters.
In some cases, you may need to create a new story with args
that have not already been defined inside the component. For example, let us create an Aside
component:
1// app/UI/Aside/Aside.tsx
2
3import React from 'react'
4
5export interface AsideProps {
6 paragraphText: string
7 children?: React.ReactNode
8}
9
10
11export default function Aside({paragraphText, children, ...props}: AsideProps) {
12
13 return(
14 <aside style={{border: '1px solid black', padding: '20px'}}>
15 <h3>aside component</h3>
16 <p>{paragraphText}</p>
17 {children}
18 </aside>
19 )
20}
Observe that the Aside
component does not have the footer
element. Create an Aside.stories.tsx
story file and create a footerText
args using the render function:
1// app/UI/Aside/Aside.stories.tsx
2
3import React from 'react'
4import type { Meta, StoryObj } from '@storybook/nextjs'
5
6import Aside from './Aside'
7
8
9export interface ExtraAsideProps {
10 footerText?: string,
11}
12
13type CustomComponentPropsAndArgs = React.ComponentProps<typeof Aside> & {footerText?: string}
14
15
16const meta = {
17 component: Aside,
18 args: {
19 paragraphText: 'main paragraph',
20 },
21 render: ({footerText, ...args}) => (
22 <Aside {...args}>
23 <footer style={{backgroundColor: 'burlywood'}}>{footerText}</footer>
24 </Aside>
25 )
26} satisfies Meta<CustomComponentPropsAndArgs>
27
28export default meta
29
30type Story = StoryObj<typeof meta>
31
32
33export const StoryOne = {
34 args: {
35 paragraphText: 'paragraph one for story one',
36 footerText: 'footer for story one',
37 }
38} satisfies Story
39
40export const StoryTwo = {
41 args: {
42 paragraphText: 'paragraph two for story two',
43 footerText: 'footer for story two'
44 }
45} satisfies Story
In the story file above, we created a TypeScript interface for type checking. Then, we passed footerText
and other possible but optional args
that are not yet declared, into the render
function as parameters. Then, we rendered the Aside
component and passed the footerText
arg as a footer
element. The footerText
args are then defined inside each story.
1type CustomComponentPropsAndArgs = React.ComponentProps<typeof Aside> & {footerText?: string}
In the code snippet above, the CustomComponentPropsandArgs
type satisfies the default export strictly, by allowing us to configure our custom props
and args
for the Aside
component. You can replace {footerText?: string}
with the ExtraAsideProps
, if you would like to include additional props
inside your component.
Updating args
for Interactive Components Using useArgs()
Create a Checkbox
component:
1// app/Forms/Checkbox/Checkbox.tsx
2'use client'
3
4export interface CheckboxProps {
5 label: string
6 onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
7 isChecked: boolean
8}
9
10export default function Checkbox({...props}: CheckboxProps) {
11
12 const { label, onChange, isChecked } = props
13
14 return(
15 <div>
16 <label htmlFor='custom-checkbox'>{label}</label>
17 <input
18 type='checkbox'
19 id='custom-checkbox'
20 name='custom-checkbox'
21 value='custom-checkbox'
22 onChange={onChange}
23 checked={isChecked}
24 />
25 </div>
26 )
27}
Next, create a Checkbox.stories.tsx
story file:
1// app/Forms/Checkbox/Checkbox.stories.tsx
2
3import type { Meta, StoryObj } from '@storybook/nextjs'
4import { useArgs } from 'storybook/preview-api';
5
6import Checkbox from './Checkbox'
7
8
9const meta = {
10 component: Checkbox,
11 args: {
12 label: 'item',
13 isChecked: false,
14 }
15
16} satisfies Meta<typeof Checkbox>
17
18export default meta
19
20type Story = StoryObj<typeof meta>
21
22export const Default = {
23 args: {
24 ...meta.args
25 },
26 render: function Render(args) {
27 const [ {isChecked}, updateArgs ] = useArgs()
28
29 function onChange() {
30 updateArgs(
31 {
32 isChecked: !isChecked
33 }
34 )
35 }
36
37 return (
38 <Checkbox {...args} label='new item' onChange={onChange} isChecked={isChecked} />
39 )
40 }
41} satisfies Story
42
43export const Unchecked = {
44 args: {
45 label: 'unchecked item',
46 isChecked: false,
47 }
48}
49
50
51export const Checked = {
52 args: {
53 label: 'checked item',
54 isChecked: true,
55 }
56}
In the story file above,
useArgs()
: works like React hooks. It helps to update theargs
value that requires user interaction.
How to Write Component Stories for Colours and Sizes
In this section, we will be writing stories for our Button
component.
Create a Button.css
file:
1// app/UI/Button/Button.css
2
3.custom-button {
4 font-family: 'Nunito Sans', 'Helvetica Neue', Helvetica, Arial, sans-serif;
5 font-weight: 700;
6 border: 0;
7 border-radius: 3em;
8 cursor: pointer;
9 display: inline-block;
10 line-height: 1;
11}
12
13.custom-button--primary {
14 color: white;
15 background-color: #1ea7fd;
16}
17
18.custom-button--secondary {
19 color: #333;
20 background-color: transparent;
21 box-shadow: rgba(0, 0, 0, 0.15) 0px 0px 0px 1px inset;
22}
23
24.custom-button--small {
25 font-size: 12px;
26 padding: 10px 16px;
27}
28
29.custom-button--medium {
30 font-size: 14px;
31 padding: 11px 20px;
32}
33
34.custom-button--large {
35 font-size: 16px;
36 padding: 12px 24px;
37}
Next, create a Button
component:
1// app/UI/Button/Button.tsx
2
3
4import './Button.css'
5
6
7export interface ButtonProps{
8 label: string;
9 mode?: 'primary' | 'secondary';
10 size?: 'small' | 'medium' | 'large';
11 className?: string;
12 onClick?: () => void;
13
14}
15
16export default function Button({...props}: ButtonProps) {
17
18 const {label, mode, className, size, ...rest} = props
19
20 const buttonMode = mode === 'primary' ? `custom-button--primary` : `custom-button--secondary`;
21 const buttonSize = size === 'small' ? 'custom-button--small' : size === 'large' ? 'custom-button--large' : 'custom-button--medium'
22 const buttonClass = className
23
24
25 return(
26 <button
27 type='button'
28 className={[`custom-button`, buttonMode, buttonSize, buttonClass].join(' ')}
29 >
30 {label}
31 </button>
32 )
33}
The Button
component above can be rendered in different colours as Primary
and Secondary
, and in different sizes as Small
, Medium
, or Large
. Create a Button.stories.tsx
story file:
1// app/UI/Button/Button.stories.tsx
2
3import type { Meta, StoryObj } from '@storybook/react'
4import { action } from 'storybook/actions'
5
6import Button from './Button'
7
8const meta = {
9 title: 'Buttons/Button',
10 component: Button,
11 args: {
12 label: 'button'
13 }
14} satisfies Meta<typeof Button>
15
16export default meta;
17
18type Story = StoryObj<typeof meta>
19
20
21export const Primary = {
22 args: {
23 label: 'Primary button',
24 mode: 'primary'
25 }
26} satisfies Story
27
28
29export const Secondary = {
30 args: {
31 label: 'Secondary button',
32 mode: 'secondary'
33 }
34} satisfies Story
35
36
37export const Small = {
38 args: {
39 label: 'Small button',
40 size: 'small'
41 }
42} satisfies Story
43
44
45export const Medium = {
46 args: {
47 label: 'Medium button',
48 size: 'medium'
49 }
50} satisfies Story
51
52
53export const Large = {
54 args: {
55 label: 'Large button',
56 size: 'large'
57 }
58} satisfies Story
59
60
61export const WithInteraction = {
62 args: {
63 label: 'click me',
64 onClick: action('button is clicked')
65 }
66} satisfies Story
Modifying Stories Visualisation using decorators
in Storybook
decorators
is an array that includes single or multiple functions. The functions allow you to render your stories by wrapping them with a markup.
In the Button.stories.tsx
file above, we want all our stories to be rendered with a margin space:
1// app/UI/Button/Button.tsx
2
3// ...
4
5const meta = {
6 title: 'Buttons/Button',
7 component: Button,
8 args: {
9 label: 'button'
10 },
11 decorators: [
12 (ButtonComponent) => (
13 <div style={{margin: '3em', padding: '10px', backgroundColor: 'grey'}}>
14 <ButtonComponent />
15 </div>
16 )
17 ],
18} satisfies Meta<typeof Button>
19
20// ...
The decorator function accepts the ButtonComponent
parameter. The parameter is rendered as a React component that is wrapped with the div
markup. The markup is styled in the way we want to visualise our stories on the Storybook UI.
How to Use decorators
at the Global level
Inside the .storybook
folder that is inside the root directory of your project, rename the preview.ts
file to preview.tsx
. Run your Storybook again using the npm run storybook
command on your terminal.
Apply the withDashedBorder
decorator function as shown in the code below:
1// .storybook/preview.tsx
2
3import type { Preview, Decorator } from '@storybook/nextjs'
4
5const withDashedBorder: Decorator = (Decoration) => (
6 <div style={{border: '1px dashed black', padding: '30px', margin: '30px'}}>
7 <Decoration />
8 </div>
9)
10
11const preview: Preview = {
12 parameters: {
13 controls: {
14 matchers: {
15 color: /(background|color)$/i,
16 date: /Date$/i,
17 },
18 },
19 },
20 decorators: [withDashedBorder]
21}
22
23export default preview
The global decorator is applied to all the stories.
Configure args
values using argTypes
argTypes
is an object that contains single or multiple args
. It allows us to restrict the values that each args
can accept. argTypes
includes the following:
control
:argTypes.[argsName].control
allows you to update yourargs
directly from the Storybook UI based on the values already declared. It includes thetype
,min
,max
,step
,accept
,labels
, or thepresetColors
property.options
: allows you to provide a list of options for eachargs
type.description
: enables you to provide additional contextual information about eachargs
.mapping
: enables you to map the exact value for eachargs
to their respective string-values that have been listed incontrol.options
.name
: enables you to change the name of theargs
. The new name will be displayed on the Storybook UI.
argTypes
follows the syntax:
1 argTypes: {
2 argsOne: {},
3 argsTwo: {},
4 argsThree: {},
5 argsFour: {},
6 }
Create a Card
component that can appear based on different background colours and opacity levels:
1// app/UI/Card/Card.tsx
2
3export interface CardProps {
4 cardBackgroundColor: string
5 cardOpacity?: number
6 cardText: string
7}
8
9export default function Card({cardBackgroundColor, cardOpacity, cardText}: CardProps) {
10
11 return (
12 <div style={{backgroundColor: `${cardBackgroundColor}`, opacity: `${cardOpacity}`}}>
13 <h3>this is a card component</h3>
14 <p>{cardText}</p>
15 </div>
16 )
17}
Next, create a Card.stories.tsx
story file:
1// app/UI/Card/Card.stories.tsx
2
3import type { Meta, StoryObj } from '@storybook/nextjs'
4import Card from './Card'
5
6
7const meta = {
8 component: Card,
9 argTypes: {
10 cardBackgroundColor: {
11 control: {
12 type: 'color',
13 }
14 },
15 cardOpacity: {
16 control: {
17 type: 'range',
18 min: 0,
19 max: 1,
20 step: 0.1,
21 }
22 },
23 cardText: {
24 control: 'text'
25 }
26 }
27} satisfies Meta<typeof Card>
28
29export default meta
30
31type Story = StoryObj<typeof meta>
32
33export const Faded = {
34 args: {
35 cardBackgroundColor: '#c40cb7',
36 cardOpacity: 0.5,
37 cardText: 'padding values are restricted'
38 }
39} satisfies Story
40
41export const Opaque = {
42 args: {
43 cardBackgroundColor: '#c40cb7',
44 cardOpacity: 0.9,
45 cardText: 'padding values are restricted'
46 }
47} satisfies Story
In the story file above:
The
cardBackgroundColor
arg type can be indicated on the Storybook UI with the use of a color picker.The
cardOpacity
arg type can be picked from a range of values that increase with a common difference of 0.1, while starting with a minimum value of 0 and ending with a maximum value of 1.The
cardText
arg type can be edited as texts that are not restricted to limited options.
Other control.type
values in argTypes
include: object
, boolean
, check
, inline-check
, radio
, inline-radio
, select
, multi-select
, number
, range
, file
, object
, color
, date
, and text
. You can read more on argTypes.
For example, the sample code below demonstrates the description of the imageSrc
arg type, and restricts the arg type to accept a .png
file only:
1argTypes: {
2 imageSrc: {
3 description: 'additional information about the imageSrc args'
4 control: {
5 type: 'file',
6 accept: '.png'
7 }
8 }
9}
To render an arg type conditionally, use the if
property. The sample code below demonstrates how to render the modalText
arg type only if the value of the isChecked
args is truthy. If the truthy
value is omitted, Storybook automatically makes the condition true.
1argTypes: {
2 isChecked: {control: 'boolean'},
3 modalText: {
4 if: {
5 arg: 'isChecked',
6 // you can equally omit the truthy value
7 truthy: true,
8 }}
9}
Other values for the if
condition include:
exists
: used to indicate ifarg
exists. It can be set totrue
orfalse
.eq
: used to indicate strict equality for thearg
value. It can be set to the required value for thearg
.neq
: used to indicate strict inequality for thearg
value. It can be set to a value that is not required for thearg
.global
: used to indicate that thearg
can only be rendered based on a specific configuredglobal
value.
The sample code below allows you to map the children
args to a specific name-value option:
1argTypes: {
2 children: {
3 control: 'radio',
4 options: ['paragraph', 'heading', 'footer']
5 mapping: {
6 paragraph: <p>paragraph text</p>,
7 heading: <h3>heading text</h3>,
8 footer: <footer>footer text</footer>,
9 }
10 }
11}
Modify Storybook Features using parameters
parameters
is an array of different configuration settings for the Storybook features and addons. These features include:
layouts
: allows you to position your stories on the Storybook UI canvas. It can be set tocentered
,fullscreen
, orpadded
.backgrounds
: allows you to specify the background color and grid display for the component stories.actions
:parameters.actions
configures the same pattern for all event handlers in a story file.docs
: for modifying the documentation pages that are rendered on the Storybook UI.options
: indicates the order in which the stories are displayed on the Storybook UI. It is applied at the global level in thepreview.ts
file only.controls
:parameters.controls
allows you to configure:the preset colors using the
presetColors
field.the restricted control panel for each story that will be displayed or not on the Storybook UI using the
include
orexclude
property.the order of arrangement for each args on the control panel using
sort
.
In the preview.tsx
global file below, we want to configure all the stories to display in the center position on the Storybook UI. Also, we want to enable additional background color settings as a parameter.
1import type { Preview, Decorator } from '@storybook/nextjs'
2
3
4// ...
5
6const preview: Preview = {
7 parameters: {
8 controls: {
9 matchers: {
10 color: /(background|color)$/i,
11 date: /Date$/i,
12 },
13 },
14 backgrounds: {
15 layouts: 'centered',
16 options: {
17 darkGreen: {name: 'dark green', value: '#283618'},
18 burntSienna: {name: 'burnt sienna', value: '#bc6c25'},
19 strongRed: {name: 'strong red', value: '#c1121f'},
20 },
21 },
22 },
23 decorators: [withDashedBorder],
24}
25
26export default preview
You can specify the initial background color for your stories by setting the initialGlobals.backgrounds
property to an already defined background color:
1// preview.tsx
2
3// ...
4
5const preview: Preview = {
6 parameters: {
7 // ...
8 },
9 decorators: [withDashedBorder],
10 // add the initial global value
11 initialGlobals: {
12 backgrounds: {value: 'darkGreen'},
13 }
14}
15export default preview
In some cases, you may want to set the background color for a specific component story. Add the globals.backgrounds
property:
1// Aside.stories.tsx
2
3// ...
4
5const meta = {
6 component: Aside,
7 // ...
8 // add the background color from the global values, and the grid display
9 globals: {
10 backgrounds: {value: 'burntSienna', grid: true}
11 },
12} satisfies Meta<CustomComponentPropsAndArgs>
If you want to disable the background color and the grid display for each component story, set the parameters.backgrounds.disable
property and the parameters.grid.disable
property to the boolean value false
respectively, as shown in the code sample below:
1// SampleComponent.Stories.tsx
2
3import { Meta, Storyobj } from '@storybook/nextjs'
4import SampleComponent from './SampleComponent'
5
6const meta = {
7 component: SampleComponent,
8 parameter: {
9 backgrounds: {disable: true},
10 grid: {disable: true},
11 }
12} satsifies Meta<typeof SampleComponent>
How to Document Stories in Storybook
When you write documentation for the resources in your design system, you enable frictionless collaboration among every member of your team, including the designers and engineers who build the design system, and the designers and engineers who use the design system. In Storybook, you can enable documentation either automatically using the autodocs
tag or by creating a .mdx
documentation file.
Enabling Automatic Documentation with autodocs
You can configure your stories to automatically create documentation either at the global level in the .storybook/preview.tsx
file, or at the component stories level in the .stories.tsx
file, by using the autodocs
tag. The documentation is generated based on the data that is derived from the stories, such as the args
, argTypes
, and parameters
.
Update the Article.stories.tsx
story file to enable automatic documentation:
1// app/UI/Article/Article.stories.tsx
2
3// ...
4
5const meta = {
6 component: Article,
7 // add the autodocs tag
8 tags: ['autodocs'],
9 args: {
10 // ...
11 },
12} satisfies Meta<typeof Article>
13
14export default meta
15
16// ...
You can locate the documentation for the component stories inside the sidebar on the Storybook UI.
Disable Automatic Documentation for Specific Component Story
If you configured automatic documentation at the global level using the tags
property: const preview: Preview = preview { tags: ['autodocs'], // ... }
, you can disable the autodocs
tag in your preferred story file by setting the tags value as shown: tags: ['!autodocs']
. The automatic documentation page for the component stories will be removed from the Storybook UI sidebar.
Create templates for Automatic Documentation
In some cases, you want your documentation to be rendered based on a defined set of templates. Templates help to form the structure of a documentation page on the Storybook UI. To create templates inside an automatic documentation, include the page
property inside the docs
parameter that is included in your story file, as shown below:
1// app/UI/Article/Article.stories.tsx
2
3// ...
4
5import { Title, Subtitle, Description, Primary, Controls, Stories, } from '@storybook/addon-docs/blocks';
6
7const meta = {
8 component: Article,
9 tags: ['autodocs'],
10 parameters: {
11 docs: {
12 // add the page property
13 page: () => (
14 <>
15 <Title />
16 <Subtitle />
17 <Description />
18 <Primary />
19 <Controls />
20 <Stories />
21 </>
22 )
23 },
24 },
25 args: {
26 // ...
27 },
28} satisfies Meta<typeof Article>
29
30export default meta
page
: is a function that returns multiple doc blocks as a single React component. The doc blocks are rendered on the Storybook UI documentation page. Doc blocks help to define the documentation structure and layout. Doc blocks are imported from@storybook/addon-docs/blocks
. As demonstrated in the story file above, each doc block has a different purpose from the other:<Title />
: provides the primary heading for the documentation.<Subtitle />
: provides the secondary heading for the documentation.<Description />
: displays the description for the default export and the component stories.<Primary />
: provides the story that is the first to be defined in the story file.<Controls />
: displays the args for a single story as a table.
Equally, you can create a separate .mdx
template file that is specific to generating templates only, and then import the template file inside the .stories.tsx
story file, or inside the .storybook/preview.tsx
global file.
Create a CardDocsTemplate.mdx
template file:
1// app/UI/Card/CardDocsTemplate.mdx
2
3import { Meta, Title, Primary, Controls, Stories } from '@storybook/addon-docs/blocks';
4
5<Meta isTemplate />
6
7<Title />
8
9<Primary />
10
11<Controls />
12
13<Stories />
In the template file above, we need to indicate that the .mdx
file is for generating templates only by adding the isTemplate
props inside the <Meta />
doc block.
Next, import the template file and render it on the parameters.docs.page
property:
1// app/UI/Card/Card.stories.tsx
2
3// ...
4
5import CardDocsTemplate from './CardDocsTemplate.mdx'
6
7const meta = {
8 component: Card,
9 tags:['autodocs'],
10 parameters: {
11 docs: {
12 page: CardDocsTemplate,
13 },
14 },
15 // ...
16} satisfies Meta<typeof Card>
17
18export default meta
Create table of contents for automatic documentation
You can generate a table of contents for easy navigation on the documentation page. Add the parameters.docs.toc
property inside the story file. The toc
value is set to the boolean value true
.
Update the Article.stories.tsx
story file:
1// app/UI/Article/Article.stories.tsx
2
3// ...
4
5const meta = {
6 component: Article,
7 tags: ['autodocs'],
8 parameters: {
9 docs: {
10 page: () => (
11 // ...
12 ),
13 // add the toc property
14 toc: true,
15 },
16 },
17 args: {
18 // ...
19 },
20} satisfies Meta<typeof Article>
21
22export default meta
The table of contents is generated based on the heading levels inside the documentation file.
Disable table of contents for a specific documentation page
If you configured the table of contents at the global level inside the .storybook/preview.tsx
file for all your automatic documentation pages, you can disable the toc
for a specific documentation page by setting the parameters.docs.toc.disable
property to true
, inside your preferred story file as shown: toc: { disable: true }
.
Enable Manual Documentation for Stories
To manually write the documentation for your stories, create a .mdx
documentation file inside the root folder of your component stories.
Create a ButtonDocs.mdx
documentation file:
1// app/UI/Button/ButtonDocs.mdx
2
3import { Meta, Canvas, Story } from '@storybook/addon-docs/blocks'
4
5import * as ButtonStories from './Button.stories';
6
7
8<Meta of={ButtonStories} />
9
10
11## Button
12
13This section explains the Button component
14
15## Canvas
16
17This section demonstrates the Canvas doc block
18
19<Canvas of={ButtonStories.Small} />
20
21
22## usage
23
24This section
25
26explains how to usage
27
28the Button component
29
30
31## Button stories
32
33This section explains the Button stories
34
35### Primary Button
36
37This section displays the Primary Button
38
39<Story of={ButtonStories.Primary} />
In the documentation file above, we included the following doc blocks:
Meta
: references the component that is being documented on the documentation page.Canvas
: allows you to view the code snippet for the story.Story
: displays the story.
Enable Custom Documentation
As your design system becomes increasingly complex with a set of principles and guidelines, you may need to create a separate documentation file outside the default documentation that is specific to component stories. Custom documentation include: quickstart guide, best practices guide, changelogs, and other markdown files that can be imported into documentation files.
To allow custom documentation in Storybook, confirm that the .storybook/main.ts
configuration file includes the docs addon. Otherwise, install the docs addon by running the command:
1npm install @storybook/addon-docs
Then, add the addon to the configuration file:
1// .storybook/main.ts
2
3import type { StorybookConfig } from '@storybook/your-framework';
4
5const config: StorybookConfig = {
6 // ...
7 // add the docs addon
8 addons: ['@storybook/addon-docs'],
9};
10
11export default config;
The docs addon allows us to write and extend custom documentation for our stories using Markdown.
Next, create a QuickStarteGuide.mdx
documentation file inside the app
directory in our Next.js project:
1// app/QuickStartGuide.mdx
2
3# Quick Start Guide
4
5This section of the documentation introduces you to the design system.
6
7## Table of Contents
8
9- [Design Guide](#design-guide)
10 - [Figma](#figma)
11 - [UI Principles](#ui-principles)
12 - [Design Assets](#design-assets)
13
14- [Development Guide](#development-guide)
15 - [Storybook](#storybook)
16 - [Version Control](#version-control)
17 - [Development Tools](#development-tools)
18
19
20## Design Guide
21
22This section is for the designers
23
24### figma
25
26This section explains Figma
27
28### UI Principles
29
30This section explains the UI principles.
31
32### Design Assets
33
34This section explains the design assets
35
36## Development Guide
37
38This section is for the developers
39
40### Storybook
41
42This section explains Storybook
43
44### Version Control
45
46This section explains version control
47
48### Development Tools
49
50This section explains the development tools
Import other Documentation files
You can import and render other documentation files into an existing documentation file by using the <Markdown />
doc block.
Inside the app
directory, create a BestPractices.md
documentation file:
1// app/BestPractices.md
2
3## Best Practices
4
5This section explains
6
7the best practices
8
9regarding the design system
Next, import and render the BestPractices.md
file inside the QuickStartGuide.mdx
file:
1// app/QuickStartGuide.mdx
2
3import BestPractices from './BestPractices.md'
4import { Markdown } from '@storybook/addon-docs/blocks';
5
6// ...
7
8<Markdown>
9 {BestPractices}
10</Markdown>
Enable linking within Documentation files
You can redirect a documentation page to another documentation page, or to another story by using the markdown hyperlink method []()
. Append the docs or story id
to the query string: ?path=docs/[id]
for docs and ?path=story/[id]
for stories. The query string for all stories or documentations can be recognised from the browser tab based on the Next.js project's file structure.
For example, the QuickStartGuide
docs have the query string ?path=/docs/app-quickstartguide--docs
, the Default
Checkbox story has the query string ?path=/story/app-forms-checkbox--default
.
Story id
can be generated:
- automatically from the story file, starting from the value of the
title
property in the default export, to the name of each story. In the demo code below, the id forStoryOne
isfolder-file--storyone
, the id forStoryTwo
isfolder-file--storytwo
:
1// SampleComponent.stories.tsx
2
3import { Meta, Storyobj } from `@storybook/nextjs`
4import SampleComponent from './SampleComponent.tsx'
5
6const meta = {
7 title: 'Folder/file',
8 component: SampleComponent,
9 args: {},
10} satisfies Meta<typeof SampleComponent>
11
12export default meta
13type Story = Storyobj<typeof meta>
14
15const StoryOne = {} satsifies Story
16
17const StoryTwo = {} satsifies Story
- manually from the
id
property in the default export and thename
property in each story. In the updatedSampleComponent.stories.tsx
story file below, the url forStoryOne
isnewfolder-newfile--newstoryone
, the url forStoryTwo
isnewfolder-newfile--newstorytwo
:
1// SampleComponent.stories.tsx
2
3// ...
4
5const meta = {
6 title: 'Folder/file',
7 component: SampleComponent,
8 // add the id property
9 id: 'newfolder-newfile',
10 args: {},
11} satisfies Meta<typeof SampleComponent>
12
13export default meta
14type Story = Storyobj<typeof meta>
15
16const StoryOne = {
17 name: 'new-storyone',
18 args: {},
19} satsifies Story
20
21const StoryTwo = {
22 name: 'new-storytwo',
23 args: {},
24} satsifies Story
Documentation id
is generated from the name of the documentation file and its parent folders. Update the QuickStartGuide.mdx
file to enable linking within our design system documentation:
1// app/QuickStartGuide.mdx
2
3// ...
4
5[Visit the Card doc page](?path=/docs/app-ui-card--docs)
6
7[Visit the usage section on the Button doc page](?path=/docs/buttons-button--buttondocs#usage)
8
9[Visit the Article.Default component story](?path=/story/app-ui-article--default)
10
11[Visit the Aside.StoryOne component story](?path=/story/app-ui-aside--story-one)
12
13[Visit the Checkbox.DefaultDuplicate component story](?path=/story/app-forms-checkbox--default-duplicate)
Change the default name for documentation pages
Inside the sidebar of the Storybook UI, you would observe that the default name for each documentation page is Docs
. You can change the default name for all the pages by adding the docs
property inside the ./storybook/main.ts
configuration file. Next, inside the docs
property, include the defaultName
property. As demonstrated in the code below, the default name is changed to 'my docs':
1// .storybook/main.ts
2
3//...
4
5const config: StorybookConfig = {
6 framework: '@storybook/your-framework',
7 stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
8 addons: ['@storybook/addon-docs'],
9 // add the docs property
10 docs: {
11 defaultName: 'my docs',
12 },
13}
14
15export default config;
How to Deploy your Storybook Design System
Before you deploy your design system to any cloud service, you will need to build Storybook as a static web application by running any of the following commands on your terminal inside the root directory of our Next.js project:
1npm run build-storybook
2# or
3pnpm run build-storybook
4# or
5yarn build-storybook
Then, preview the static web application locally on your web server by running the command:
1npx serve storybook-static
Here, we will be deploying our design system to Chromatic. Ensure that you have the project pushed to a GitHub repository.
Deploy to Chromatic
Chromatic is a visual testing & review tool that can catch both visual and functional bugs.
Create a Chromatic account and login to your account. Click on the Add Project button on the Chromatic UI and select your project from the GitHub repository.
Next, inside the root directory for your project, install Chromatic by running the command:
1npm install --save-dev chromatic
2# or
3yarn add --dev chromatic
Then, add the project token. The project token allows you to connect your project to Chromatic securely. The project token can be identified from the Chromatic website after you have created your project. Click the manage tab, next, click the configure tab to view your project token.
Run this command on your terminal:
1npx chromatic --project-token=<project token>
Visit the deployed design system on the website
Setup Continuous Integration
Continuous Integration (CI) allows you to automatically merge the changes made to your code repository before your project is deployed again.
Create a chromatic.yml
file inside a .github/workflows
directory. We will be setting up our project with GitHub Actions.
1// .github/workflows/chromatic.yml
2
3name: "Chromatic"
4
5on: push
6
7jobs:
8 chromatic:
9 name: Run Chromatic
10 runs-on: ubuntu-latest
11 steps:
12 - name: Checkout code
13 uses: actions/checkout@v4
14 with:
15 fetch-depth: 0
16 - uses: actions/setup-node@v4
17 with:
18 node-version: 22.17.0
19 - name: Install dependencies
20 # ⚠️ See your package manager's documentation for the correct command to install dependencies in a CI environment.
21 run: npm ci
22 - name: Run Chromatic
23 uses: chromaui/action@latest
24 with:
25 # ⚠️ Make sure to configure a `CHROMATIC_PROJECT_TOKEN` repository secret
26 projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
To connect your project token to GitHub, navigate to the settings tab on GitHub. Then, navigate to Security, click on Secrets and variables, click Actions, then click New repository secret. Set the Name of the secret as CHROMATIC_PROJECT_TOKEN
. Paste the project token that you copied from the Chromatic website.
If you would like to deploy your design system to Vercel, check this guide.
Conclusion
In this article, we have defined the reasons we need to build design systems and the approach to building, documenting, and deploying design systems. We implemented the use of Storybook APIs such as args
, argTypes
, decorators
, and parameters
to build a maintainable design system.
Product Adoption Strategist (Talks about PLG).