דלג לתוכן הראשי

Guide for Storybook

1. Getting started

1.1. Installation

bluh bluh bluh

1.2. Configure Storybook with Tailwind

To have Storybook and tailwind working together is rather simple.
All Storybook needs is a css, generated by tailwind as an output, as its input.
That same css file needs to be imported in the preview.js configuration file of Storybook:

.storybook/preview.js
import '../styles/tailwind.css';

/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};

export default preview;

This will make Tailwind’s style classes available to all of your stories.
To generate that css file with tailwind, all you gotta do is wrote a script that does so:

package.json
{
"pre-storybook-prod": "npx tailwindcss -o ./styles/tailwind.css --minify"
}

This is for production.
For development, you would most-likely want a watch version of that script, so that you could make changes to the source code, and see changes in real-time:

package.json
{
"pre-storybook-dev": "npx tailwindcss -i ./src/index.css -o ./styles/tailwind.css --watch"
}

1.3. Configure Storybook With Dark Mode

First of all, update your tailwind.config.js file to change themes based on a class or data-attribute. This example uses a data-attribute.

tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
// Toggle dark-mode based on .dark class or data-mode="dark"
darkMode: ['class', '[data-theme="dark"]'],
theme: {
extend: {},
},
plugins: [],
};

Next, install the @storybook/addon-themes addon to provide the switcher tool.

npm i -D @storybook/addon-themes

Then, add following content to .storybook/main.js:

.storybook/main.js
export default {
addons: ['@storybook/addon-themes'],
};

Toggle themes by class name

Add the withThemeByClassName decorator to your Storybook from @storybook/addon-themes:

.storybook/preview.js
import { withThemeByClassName } from '@storybook/addon-themes';

/* snipped for brevity */

export const decorators = [
withThemeByClassName({
themes: { light: 'light', dark: 'dark' },
defaultTheme: 'light',
}),
];

Toggle themes by data-attribute

Add the withThemeByDataAttribute decorator to your Storybook from @storybook/addon-themes:

.storybook/preview.js
import { withThemeByDataAttribute } from '@storybook/addon-themes';

/* snipped for brevity */

export const decorators = [
withThemeByDataAttribute({
themes: {
light: 'light',
dark: 'dark',
},
defaultTheme: 'light',
attributeName: 'data-mode',
}),
];

2. The *.stories.jsx Files

Storybook scans your project and looks for files which end with: .stories.js, .stories.jsx, .stories.ts, .stories.tsx.
Notice how it has stories in it's path, in plural, to note that each file representing a component can export multiple stories. A *.stories.js file defines all the stories for a component. Each story has a corresponding sidebar item in the Storybook app. When you click on a story, it renders in the Canvas an isolated preview iframe.

USE ONLY .stories.jsx with jsx extension!!

If you use .js extension, than jsx you write would result in Storybook crashing! With a useless explanation as to why!

3. A Component's Meta

Each .stories.jsx file must include a Component's meta, and export default it.
The meta is simply a javascript object with properties.
At the very least, the meta object must contain the component key, which points to the actual component. The default export metadata controls how Storybook lists your stories and provides information used by addons. For example, here’s the default export for a story file Button.stories.js|ts:

Button.stories.jsx
import Button from './Button';

export default {
component: Button,
};
info

Starting with Storybook version 7.0, story titles are analyzed statically as part of the build process. The default export must contain a title property that can be read statically or a component property from which an automatic title can be computed. Using the id property to customize your story URL must also be statically readable.

4. Layout Centered

Another nice-to-have key inside meta is the parameters.layout, which tells Storybook where to render the component on the screen. By default, it renders it on the top-left, but it would be nice to have it centered, right? To do so, simply add:

Button.stories.jsx
import Button from './Button';

export default {
component: Button,
parameters: {
// Optional parameter to center the component in the Canvas. More info: https://storybook.js.org/docs/react/configure/story-layout
layout: 'centered',
},
};

5 Writing a Story

5.1. Introduction

A story is merely a javascript object which hold an args key. combination of values for the component's props, which describes how to render the component.
A story needs to be named-exported from the *stories.js file.
A story with an empty as an object, will simply mean that all the component's props are undefined.
The variable name holding the story will be the name presented in the Storybook app, describing that story, so it's a good idea to have it uppercased. If a stories.js file does not export a single story, no visuals of that component would appear in the Storybook app. It would be like the component doesn't even exist.

YourComponent.stories.jsx
import { YourComponent } from './YourComponent';

// 👇 This default export determines where your story goes in the story list
export default {
component: YourComponent,
};

export const FirstStory = {
args: {
// 👇 The args you need here will depend on your component
},
};

5.2. Defining stories

Use the named exports of a file to define your component’s stories. We recommend you use UpperCamelCase for your story exports. Here’s how to render Button in the "primary" state and export a story called Primary.

Button.stories.jsx
import { Button } from './Button';

export default {
component: Button,
};

/*
*👇 Render functions are a framework specific feature to allow you control on how the component renders.
* See https://storybook.js.org/docs/api/csf
* to learn how to use render functions.
*/
export const Primary = {
render: () => <Button primary label='Button' />,
};

5.3. Rename stories

You can rename a story to give it a more accurate display name using the name property on your story object.
Here's an example:

Button.stories.jsx
import { Button } from './Button';

export const Primary = {
name: 'I am the primary',
render: () => <Button primary label='Button' />,
};

export default { component: Button };

5.4. Story level args

Obviously we've seen those already. These are the args defined on each story:

const Default = {
name: 'Default Case',
args: {
isPrimary: true,
color: 'blue',
disabled: false,
},
};

These are the strongest args, and will take precedence over Component level args & global level args.

5.5 Story Custom Render

Here's how you can have a custom-made renderer for your story:

export const Primary = {
args: { isPrimary: true, label: 'Button' },
render: (args) => (
const { backgroundColor, isPrimary, label, size } = args;
// 👇 Assigns the function result to a variable
const someFunctionResult = someFunction(propertyA, propertyB);

<Button {...args} someComplexProp={someFunctionResult}/>
),
};

This could be useful in multiple cases.
For example, in a case where you need the parent to have dir="rtl".

5.6. Hide an arg's controller

If you wish to hide a certain arg, or I should say a controller for an arg, there's a very easy way to do so. Let's say you have a Component with a prop named testId, and you decided you don't need a controller for it.
You have two options as to how you can hide a prop's controller.

    1. The direct way: using meta.argTypes.propName1.table.disable

The direct way is using the meta.argTypes.propName1.table.disable key and provide a boolean false to hide it from view.

// <typeof Button>
/** @type {import('@storybook/react').Meta} */
export default {
title: 'Example/Button',
component: Button,
argTypes: { testId: { table: { disable: true } } },
};
    1. The indirect way: using meta.parameters.controls.exclude

The indirect way is using the meta.parameters.controls.exclude key and provide a regex that catches the arg by its name, to hide it from view.

/** @type {import('@storybook/react').Meta} */
export default {
title: 'Example/Button',
component: Button,
parameters: {
controls: { exclude: /testId/g },
},
};

5.7 Storybook Controls

- Introduction

In this section you'll learn how to write Docs for your components.
Under Meta, add an argsType key, which should be an object.
Each key under argsType is actually prop name of your component.

/** @type {import('@storybook/react').Meta<typeof Button>} */
export default {
title: 'Example/Button',
component: Button,
parameters: { layout: 'centered' },
tags: ['autodocs'],
argTypes: {
propName1: { ... },
propName2: { ... },
},
};

- Adding docs

Every prop name cab have basic metadata such as name, description, and defaultValue.

/** @type {import('@storybook/react').Meta<typeof Button>} */
export default {
title: 'Example/Button',
component: Button,
parameters: { layout: 'centered' },
tags: ['autodocs'],
argTypes: {
propName1: {
name: 'This will replace `propName1`',
description: 'This is the description for `propName1`',
defaultValue: 111,
control: ...,
},
},
};

- Choosing the control type

  • Control Type 1: boolean

Provides a toggle for switching between possible states.

export default {
component: Button,
argTypes: {
propName1: {
control: 'boolean',
},
},
};
  • Control Type 2: text

Provides a freeform text input.

export default {
component: Button,
argTypes: {
propName1: 'text',
},
};
  • Control Type 3: number

Provides a numeric input to include the range of all possible values.

export default {
component: Button,
argTypes: {
propName1: {
control: 'number',
min: 1,
max: 30,
step: 2,
},
},
};
  • Control Type 4: range

Provides a range slider component to include all possible values.

export default {
component: Button,
argTypes: {
propName1: {
control: {
type: 'range',
min: 1,
max: 30,
step: 3,
},
},
},
};
  • Control Type 5: object

Provides a JSON-based editor component to handle the object's values. Also allows edition in raw mode. It is also how you handle an array type.

export default {
component: Button,
argTypes: {
propName1: {
control: 'object',
},
},
};
  • Control Type 6: radio & inline-radio

Provide a set of radio/inline-radio buttons based on the available options.

export default {
component: Button,
argTypes: {
propName1: {
control: 'radio', // <--- or 'inline-radio'
options: ['email', 'phone', 'mail'],
},
},
};
  • Control Type 7: check & inline-check

Provide a set of checkbox components for selecting multiple options.

export default {
component: Button,
argTypes: {
propName1: {
control: 'check', // <--- or 'inline-check'
options: ['email', 'phone', 'mail'],
},
},
};
  • Control Type 8: select & multi-select

Provide a drop-down list component to handle single value selection.

export default {
component: Button,
argTypes: {
propName1: {
control: 'select',
options: [20, 30, 40, 50],
},
},
};

The options can be renamed to something else by using the labels key:

export default {
component: Button,
argTypes: {
propName1: {
options: ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'], // An array of serializable values
control: {
type: 'select', // Type 'select' is automatically inferred when 'options' is defined
labels: {
ArrowUp: 'Up',
ArrowDown: 'Down',
ArrowLeft: 'Left',
ArrowRight: 'Right',
},
},
},
},
};
  • Control Type 9: color

Provides a color picker component to handle color values. Can be additionally configured to include a set of color presets.

export default {
component: Button,
argTypes: {
propName1: {
control: {
type: 'color',
presetColors: ['red', 'green', 'blue'],
},
},
},
};

Specify initial preset color swatches

For color controls, you can specify an array of presetColors, either on the control in argTypes, or as a parameter under the controls namespace:

export default {
parameters: {
controls: {
presetColors: [{ color: '#ff4785', title: 'Coral' }, 'rgba(0, 159, 183, 1)', '#fe4a49'],
},
},
};

Color presets can be defined as an object with color and title or a simple CSS color string. These will then be available as swatches in the color picker. When you hover over the color swatch, you'll be able to see its title. It will default to the nearest CSS color name if none is specified.

  • Control Type 10: date

Provides a date-picker component to handle date selection.

export default {
component: Button,
argTypes: {
propName1: {
control: 'date',
},
},
};
  • Control Type 11: file

Provides a file input component that returns an array of URLs. Can be further customized to accept specific file types.

export default {
component: Button,
argTypes: {
propName1: {
control: {
type: 'file',
accept: '.png',
},
},
},
};

5.8. Conditional controls

In some cases, it's useful to be able to conditionally exclude a control based on the value of another control. Controls supports basic versions of these use cases with the if, which can take a simple query object to determine whether to include the control.

Consider a collection of "advanced" settings that are only visible when the user toggles an "advanced" toggle.

import { Button } from './Button';

export default {
component: Button,
argTypes: {
label: { control: 'text' }, // Always shows the control
advanced: { control: 'boolean' },
// Only enabled if advanced is true
margin: { control: 'number', if: { arg: 'advanced' } },
padding: { control: 'number', if: { arg: 'advanced' } },
cornerRadius: { control: 'number', if: { arg: 'advanced' } },
},
};

Or consider a constraint where if the user sets one control value, it doesn't make sense for the user to be able to set another value.

import { Button } from './Button';

export default {
component: Button,
argTypes: {
// Button can be passed a label or an image, not both
label: {
control: 'text',
if: { arg: 'image', truthy: false },
},
image: {
control: { type: 'select', options: ['foo.jpg', 'bar.jpg'] },
if: { arg: 'label', truthy: false },
},
},
};

It may also contain at most one of the following operators:

OperatorTypeMeaning
truthybooleanIs the target value truthy?
existsbooleanIs the target value defined?
eqanyIs the target value equal to the provided value?
neqanyIs the target value NOT equal to the provided value?

If no operator is provided, that is equivalent to { truthy: true }.

6. Public - Static Serve

You may find yourself in need for fetching resources from a public static folder. Storybook allows you to link to static files in your project or stories.

  • Step 1
    Either create a folder named public inside .storybook, or use your frontend project's public directory (depending on where your storbook project lives).

  • Step 2
    Go to your .storybook/main.js file, and add a staticDirs key, which accepts an array of strings. Each string is a path to a public folder you want to serve.

    .storybook/main.js
    export default {
    framework: '@storybook/your-framework',
    stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
    staticDirs: ['../public'], // 👈 Configures the static asset folder in Storybook
    };

    Another approach is one that allows for renaming of the output folder:

    .storybook/main.js
    export default {
    framework: '@storybook/your-framework',
    stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
    staticDirs: [{ from: '../my-custom-assets/images', to: '/assets' }],
    };```

We recommend serving assets/resources from the public folder inside ..storybook, and not from external resources, to ensure that assets are always available to your stories.

warning

Deprecated

There is another approach to serving static files but it is deprecated, and that is by using storybook's CLI with the flag of --static-dir or -s. What we did above was serving static files via the configuration file (.storybook/main.js) which replaced the serving of static files using storybook's CLI. Avoid using it! Also, the path you need to mention for the dev script and the path you need to mention for the build script are NOT the same!!! Each script requires a different path.

package.json
{
"scripts": {
"storybook": "storybook dev -p 6006 -s public", // <--- don't use this approach
"build-storybook": "storybook build -s .storybook/public" // <--- don't use this approach
}
}

7. Using Decorators for Context Providers

Decorators are a mechanism to wrap a component in arbitrary markup when rendering a story. Components are often created with assumptions about ‘where’ they render. Your styles might expect a theme or layout wrapper, or your UI might expect specific context or data providers.

A simple example is adding padding to a component’s stories. Accomplish this using a decorator that wraps the stories in a div with padding, like so:

Button.stories.jsx
import { Button } from './Button';

export default {
component: Button,
decorators: [
(Story) => (
<div style={{ margin: '3em' }}>
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
<Story />
</div>
),
],
};

If a particular story has a problem rendering, often it means your component expects a specific environment is available to the component.

A common frontend pattern is for components to assume that they render in a specific "context" with parent components higher up the rendering hierarchy (for instance, theme providers).

Use decorators to "wrap" every story in the necessary context providers. The .storybook/preview.js file allows you to customize how components render in Canvas - the preview iframe.

.storybook/preview.js
import React from 'react';
import { ThemeProvider } from 'styled-components';

export default {
decorators: [
(Story) => (
<ThemeProvider theme='default'>
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
<Story />
</ThemeProvider>
),
],
};

Or...

.storybook/preview.js
import { withThemeByDataAttribute } from '@storybook/addon-themes';
import '../styles/tailwind.css';

export const decorators = [
// Applies only if tailwind's darkMode config is set to 'class':
// withThemeByClassName({
// themes: { light: 'light', dark: 'dark' },
// defaultTheme: 'light',
// parentSelector: 'body',
// }),
// Applies only if tailwind's darkMode config is set to ['class', '[data-theme="dark"]']:
withThemeByDataAttribute({
themes: { light: 'light', dark: 'dark' },
defaultTheme: 'light',
attributeName: 'data-theme',
}),
];

/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i, // <--- if a react prop name contains these, the selected controller would be a color-picker.
date: /Date$/i, // <--- if a react prop name contains this, the selected controller would be a date-picker.
},
},
},
};

export default preview;

8. Levels of args

There are 3 levels of args: Story, Component, and Global.
Story level we've already covered, so let's jump into the other two.

- Component lvl args

You can also define args at the component level; they will apply to all the component's stories unless you overwrite them with story args. To define component args, use the args key on the default export:

Button.stories.jsx
import { Button } from './Button';

export default {
component: Button,
// 👇 Creates specific argTypes
argTypes: {
backgroundColor: { control: 'color' },
},
args: {
// 👇 Now all Button stories will be primary.
primary: true,
},
};

- Global lvl args

You can also define args at the global level; they will apply to every component's stories unless you overwrite them. To do so, define the args property in the default export of preview.js:

Button.stories.jsx
export default {
// The default value of the theme arg for all stories
args: { theme: 'light' },
};
tip

For most uses of global args, globals are a better tool for defining globally-applied settings, such as a theme. Using globals enables users to change the value with the toolbar menu.

9. Override theme on the component level

Button.stories.jsx
export default {
parameters: {
themes: {
themeOverride: 'light', // component level override
},
},
};

10. Loaders

Loaders are asynchronous functions that load data for a story and its decorators. A story's loaders run before the story renders, and the loaded data injected into the story via its render context.

Loaders can be used to load any asset, lazy load components, or fetch data from a remote API. This feature was designed as a performance optimization to handle large story imports. However, args is the recommended way to manage story data. We're building up an ecosystem of tools and techniques around Args that might not be compatible with loaded data.

They are an advanced feature (i.e., escape hatch), and we only recommend using them if you have a specific need that other means can't fulfill.

Stories are isolated component examples that render internal data defined as part of the story or alongside the story as args.

Loaders are helpful when you need to load story data externally (e.g., from a remote API). Consider the following example that fetches a todo item to display in a todo list:

TodoItem.stories.jsx
import { TodoItem } from './TodoItem';

export default {
component: TodoItem,
render: (args, { loaded: { todo } }) => <TodoItem {...args} {...todo} />,
};

export const Primary = {
loaders: [
async () => {
// Servier-side code here!!!
const data = await fetch('https://jsonplaceholder.typicode.com/todos/1').then((response) => response.json());

return { todo: data };
},
],
};

Global Loaders

We can also set a loader for all stories via the loaders export of your .storybook/preview.js file (this is the file where you configure all stories):

export default {
loaders: [
async () => ({
currentUser: await (await fetch('https://jsonplaceholder.typicode.com/users/1')).json(),
}),
],
};

999 Misc

The "Docs" page displays auto-generated documentation for components (inferred from the source code). Usage documentation is helpful when sharing reusable components with your team, for example, in an application.

#NOTE!!! decorators & render does not work in .js files.


decorators

export default {
decorators: [
(Story) => (
<div style={{ margin: '0', padding: '1em', border: '1px solid black' }}>
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
<Story />
</div>
),
],
};

sorting your stories

import '../styles/tailwind.css';

/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
options: {
// The `a` and `b` arguments in this function have a type of `import('@storybook/types').IndexEntry`. Remember that the function is executed in a JavaScript environment, so use JSDoc for IntelliSense to introspect it.
storySort: (a, b) => (a.id === b.id ? 0 : a.id.localeCompare(b.id, undefined, { numeric: true })),
},
},
};

export default preview;

2.4. Actions to enhance args

Addons can enhance args. For instance, Actions auto-detects which args are callbacks and appends a logging function to them. That way, interactions (like clicks) get logged in the actions panel.

2.5. Using the play function

... complete this part...

Storybook's play function and the @storybook/addon-interactions are convenient helper methods to test component scenarios that otherwise require user intervention. They're small code snippets that execute once your story renders. For example, suppose you wanted to validate a form component, you could write the following story using the play function to check how the component responds when filling in the inputs with information:

LoginForm.stories.jsx
import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library';
import { LoginForm } from './LoginForm';

export default {
component: LoginForm,
};

export const EmptyForm = {};

export const FilledForm = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);

// 👇 Simulate interactions with the component
await userEvent.type(canvas.getByTestId('email'), 'email@provider.com');

await userEvent.type(canvas.getByTestId('password'), 'a-random-password');

// See https://storybook.js.org/docs/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
await userEvent.click(canvas.getByRole('button'));

// 👇 Assert DOM structure
await expect(
canvas.getByText(
'Everything is perfect. Your account is ready and we should probably get you started!',
),
).toBeInTheDocument();
},
};

Without the help of the play function and the @storybook/addon-interactions, you had to write your own stories and manually interact with the component to test out each use case scenario possible.


add background options to storybook

import { withThemeByDataAttribute } from '@storybook/addon-themes';
import '../styles/tailwind.css';

// import 'tailwindcss/tailwind.css'; <--- an index.css file from tailwind inside node-modules, which only contains the 3 lines.

export const decorators = [
// Applies only if tailwind's darkMode config is set to 'class':
// withThemeByClassName({
// themes: { light: 'light', dark: 'dark' },
// defaultTheme: 'light',
// parentSelector: 'body',
// }),
// Applies only if tailwind's darkMode config is set to ['class', '[data-theme="dark"]']:
withThemeByDataAttribute({
themes: { light: 'light', dark: 'dark' },
defaultTheme: 'light',
attributeName: 'data-theme',
}),
];

/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
backgrounds: {
default: 'Twitter',
values: [
{ name: 'Twitter', value: '#00aced' },
{ name: 'Facebook', value: '#3b5998' },
{ name: 'White', value: '#fff' },
{ name: 'Black', value: '#000' },
],
},
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i, // <--- if a react prop name contains these, the selected controller would be a color-picker.
date: /Date$/i, // <--- if a react prop name contains this, the selected controller would be a date-picker.
},
},
docs: {
// `toc` Accepts `true` or an object. Generates a table-of-content on the side of the Docs page of each component. Defaults to false.
toc: {
disable: false,
title: 'Table of Contents', // <--- give it a title. Defaults to nothing being displayed.
headingSelector: 'h2, h3', // <--- Defines the list of headings to feature in the table of contents.
// ignoreSelector: 'h1', // <--- or '#primary',
// unsafeTocbotOptions: { orderedList: true },
},
},
},
};

export default preview;

You can also define backgrounds per-component or per-story basis through parameter inheritance. It's the same parameters key after all.

If for some weird reason you want to disable a background on a specific story, you can do it like so:

import { Button } from './Button';

export const Large = { parameters: { backgrounds: { disable: true } } };

export default { component: Button };

You also have control over the grid coming from the grid addon.
The default values of the grid are:

import '../styles/tailwind.css';

/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
backgrounds: {
grid: {
cellSize: 20,
opacity: 0.5,
cellAmount: 5,
offsetX: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
offsetY: 16, // Default is 0 if story has 'fullscreen' layout, 16 if layout is 'padded'
},
},
},
};

export default preview;

If for some weird reason you want to disable the grid for a specific Story you can do so like this:

import { Button } from './Button';

export const Large = {
parameters: {
backgrounds: {
grid: { disable: true },
},
},
};

export default { component: Button };