Writing Addons

This is a complete guide on how to create addons for Storybook.

Storybook Basics

Before we begin, we need to learn a bit about how Storybook works. Basically, Storybook has a Manager App and a Preview Area.

Manager App is the client side UI for Storybook. Preview Area is the place where the story is rendered. Usually the Preview Area is an iframe.

When you select a story from the Manager App, the relevant story is rendered inside the Preview Area.

Storybook Components

As shown in the above image, there’s a communication channel that the Manager App and Preview Area use to communicate with each other.

Capabilities

With an addon, you can add more functionality to Storybook. Here are a few things you could do:

  • Add a panel to Storybook (like Action Logger).
  • Add a tool to Storybook (like zoom or grid).
  • Add a tab to Storybook (like notes).
  • Interact/communicate with the iframe/manager.
  • Interact/communicate with other addons.
  • Change storybook’s state using it’s APIs.
  • Navigating.
  • Register keyboard shortcuts (coming soon).With this, you can write some pretty cool addons. Look at our Addon gallery to have a look at some sample addons.

Getting Started

Let’s write a simplistic addon for Storybook. We’ll add some metadata to the story, which the addon can then use.

Add simple metadata using parameters

We write a story for our addon like this:

  1. import React from 'react';
  2. import { storiesOf } from '@storybook/react';
  3. import Button from './Button';
  4. storiesOf('Button', module)
  5. .add('with text', () => <Button>Hello Button</Button>, {
  6. myAddon: {
  7. data: 'this data is passed to the addon',
  8. },
  9. });

Add a panel

We write an addon that responds to a change in story selection like so:

  1. // register.js
  2. import React from 'react';
  3. import { STORY_RENDERED } from '@storybook/core-events';
  4. import { addons, types } from '@storybook/addons';
  5. import { useParameter } from '@storybook/api';
  6. import { AddonPanel } from '@storybook/components';
  7. const ADDON_ID = 'myaddon';
  8. const PARAM_KEY = 'myAddon';
  9. const PANEL_ID = `${ADDON_ID}/panel`;
  10. const MyPanel = () => {
  11. const value = useParameter(PARAM_KEY, null);
  12. return <div>{value}</div>;
  13. }
  14. addons.register(ADDON_ID, api => {
  15. const render = ({ active, key }) => (
  16. <AddonPanel active={active} key={key}>
  17. <MyPanel />
  18. </AddonPanel>
  19. );
  20. const title = 'My Addon';
  21. addons.add(PANEL_ID, {
  22. type: types.PANEL,
  23. title,
  24. render,
  25. paramKey: PARAM_KEY,
  26. });
  27. });

register the addon

Then create an addons.js inside the Storybook config directory and add the following content to it.

  1. import 'path/to/register.js';

Now restart/rebuild storybook and the addon should show up!When changing stories, the addon’s onStoryChange method will be invoked with the new storyId.

Note:

If you get an error similar to:

  1. ModuleParseError: Module parse failed: Unexpected token (92:22)
  2. You may need an appropriate loader to handle this file type.
  3. | var value = this.state.value;
  4. | var active = this.props.active;
  5. > return active ? <div>{value}</div> : null;
  6. | }
  7. | }]);

It is likely because you do not have a .babelrc file or do not have it configured with the correct presets { "presets": ["@babel/preset-env", "@babel/preset-react"] }

A more complex addon

If we want to create a more complex addon, one that wraps the component being rendered for example, there are a few more steps.Essentially you can start communicating from and to the manager using the storybook API.

Now we need to create two files, register.js and index.js,. register.js will be loaded by the manager (the outer frame) and index.js will be loaded in the iframe/preview. If you want your addon to be framework agnostic, THIS is the file where you need to be careful about that.

Creating a decorator

Let’s add the following content to the index.js. It will expose a decorator called withFoo which we use the .addDecorator() API to decorate all our stories.

The @storybook/addons package contains a makeDecorator function which we can easily use to create such a decorator:

  1. import React from 'react';
  2. import addons, { makeDecorator } from '@storybook/addons';
  3. export default makeDecorator({
  4. name: 'withMyAddon',
  5. parameterName: 'myParameter',
  6. // This means don't run this decorator if the notes decorator is not set
  7. skipIfNoParametersOrOptions: true,
  8. wrapper: (getStory, context, { parameters }) => {
  9. const channel = addons.getChannel();
  10. // Our simple API above simply sets the notes parameter to a string,
  11. // which we send to the channel
  12. channel.emit('my/customEvent', parameters);
  13. // we can also add subscriptions here using channel.on('eventName', callback);
  14. return getStory(context);
  15. }
  16. })

In this case, our component can access something called the channel. It lets us communicate with the panel (in the manager).It has a NodeJS EventEmitter compatible API.

In the above case, it will emit the notes’ text to the channel, so our panel can listen to it.

Then add the following code to the register.js.

The storybook API itself has .on(), .off() and .emit() methods just like the EventEmitter.

A very convenient way of using the channel in the manager is using the useChannel hook.

  1. import React from 'react';
  2. import addons from '@storybook/addons';
  3. import { useChannel } from '@storybook/api';
  4. import { STORY_CHANGED } from '@storybook/core-events';
  5. const MyPanel = () => {
  6. const emit = useChannel({
  7. STORY_RENDERED: id => { /* do something */ },
  8. 'my/customEvent': () => { /* so something */ },
  9. });
  10. return <button onClick={() => emit('my/otherEvent')}>click to emit</button>;
  11. }
  12. // Register the addon with a unique name.
  13. addons.register('my/addon', api => {
  14. // Also need to set a unique name to the panel.
  15. addons.addPanel('my/addon/panel', {
  16. title: 'My Addon',
  17. render: ({ active, key }) => (
  18. <AddonPanel key={key} active={active}>
  19. <MyPanel />
  20. </AddonPanel>
  21. ),
  22. });
  23. });

It will register our addon and add a panel. In that we’ll render React component called MyPanel.

Using the hook, we’ll listen for events and gain access to the emit function for emitting events from our component.

In this example, we are only sending messages from the Preview Area to the Manager App (our panel).

It also listens to another event, called onStory, in the storybook API, which fires when the user selects a story.

Multiple addons can be loaded, but only a single panel can be shown, the render function will receive an active prop, which is true if the addon is shown. It is up to you to decide if this mean your component must be unmounted, or just visually hidden. This allows you to keep state but unmount expensive renderings.

The AddonPanel component will stop rendering of it’s children if it’s active-prop is false.

A great way of preserving state even when your component is unmounted is using the useAddonState hook:

  1. export const Panel = () => {
  2. const [state, setState] = useAddonState('my/addon-id', 'initial state');
  3. return (
  4. <button onClick={() => setState('a new value')}>
  5. the state = "{state}"
  6. </button>
  7. );
  8. }

This will store your addon’s state into storybook core state, and so when your component gets unmounted & remounted, the state will just be restored.

This is also a great way to sync state between multiple components of the same addon.

Using the complex addon

Add the register.js to your addons.js file.

Then you need to start using the decorator:

  1. import React from 'react';
  2. import { storiesOf } from '@storybook/react';
  3. import withMyAddon from 'path/to/index.js';
  4. import Button from './Button';
  5. storiesOf('Button', module)
  6. .addDecorator(withMyAddon)
  7. .add('with text', () => <Button>Hello Button</Button>, {
  8. myParameter: {
  9. data: 'awesome',
  10. },
  11. });

Disabling an addon panel

It’s possible to disable an addon panel for a particular story.

To offer that capability, you need to pass the paramKey when you register the panel

  1. addons.register(ADDON_ID, () => {
  2. addons.add(PANEL_ID, {
  3. type: types.PANEL,
  4. title: 'My Addon',
  5. render: () => <div>Addon tab content</div>,
  6. paramKey: 'myAddon',
  7. });
  8. });

While adding a story, you can then pass a disabled parameter.

  1. storiesOf('Button', module)
  2. .add('with text', () => <Button>Hello Button</Button>, {
  3. myAddon: {
  4. disabled: true,
  5. },
  6. });

Styling your addon

We use emotion for styling, AND we provide a theme which can be set by the user!

We highly recommend you also use emotion to style your components for storybook, but it’s not a requirement. You can use inline styles or another css-in-js lib. You can receive the theme as a prop by using the withTheme hoc from emotion. Read more about theming.

But if you do use emotion, you can use the active storybook theme, which benefits users.

Re-using existing components

Wouldn’t it be awesome if we provided you with some common used components you could use to build out your own addon quickly and fit in right away?Good news! WE DO! We publish most of storybook’s UI components as a package: @storybook/components. You can check them out in our storybook (pretty meta right?).

Addon API

Here we’ve only used a few functionalities of our Addon API.You can learn more about the complete API here.

Packaging

You can package this addon into a NPM module very easily. As an example, have a look at this package.

In addition to moving the above code to a NPM module, we’ve set react and @storybook/addons as peer dependencies.

Local Development

When you are developing your addon as a package, you can’t use npm link to add it to your project. Instead add your package as a local dependency into your package.json as shown below:

  1. {
  2. "dependencies": {
  3. "@storybook/addon-notes": "file:///home/username/myrepo"
  4. }
  5. }

Package Maintenance

Your packaged Storybook addon needs to be written in ES5. If you are using ES6, then you need to transpile it.