How to Create Custom Storybook Addons: A Complete Developer Guide

Storybook addons are plug-in modules that extend the Storybook UI using the storybook/manager-api for toolbar/panel interactions and storybook/preview-api for iframe modifications, bundled via tsup into separate manager and preview entry points.

Creating custom Storybook addons allows you to add bespoke tooling, custom controls, or specialized workflows directly into the Storybook interface. Whether you need a toolbar toggle for CSS pseudo-states or a dedicated panel for design tokens, the addon API in the storybookjs/storybook repository provides the hooks and components necessary to integrate seamlessly with both the manager UI and the story preview iframe.

Understanding Storybook Addon Architecture

Storybook addons operate across two distinct runtime environments that communicate via a dedicated channel.

The manager is the outer Storybook UI shell containing the sidebar, toolbar, and addon panels. The preview is the iframe that renders your actual components. Addons targeting the manager use storybook/manager-api to register UI elements, while addons affecting story rendering use storybook/preview-api to inject decorators or modify parameters.

Package Purpose
storybook/manager-api Interacts with the manager UI (toolbar, panels, tabs) and provides hooks such as useGlobals, useStorybookApi, and useAddonState.
storybook/preview-api Configures the preview side (decorators, parameters, args) and lets the addon modify story rendering.

Anatomy of a Custom Addon

A typical UI-based addon consists of four core pieces working together to extend Storybook functionality.

Registration and UI Types

Every addon starts with a registration script that declares a unique ID and specifies where the UI appears. In code/addons/pseudo-states/src/manager.ts, the addon registers itself as a toolbar tool using addons.register and addons.add:

import { addons, types } from 'storybook/manager-api';
import { ADDON_ID, TOOL_ID } from './constants';
import { PseudoStateTool } from './manager/PseudoStateTool';

addons.register(ADDON_ID, () => {
  addons.add(TOOL_ID, {
    type: types.TOOL,
    title: 'CSS pseudo states',
    match: ({ viewMode }) => viewMode === 'story',
    render: PseudoStateTool,
  });
});
  • addons.register serves as the entry point for every custom Storybook addon.
  • addons.add describes the UI element, including its type (types.TOOL, types.PANEL, or types.TAB), display title, and React component.
  • The match function controls visibility, allowing you to restrict the addon to story mode while hiding it in docs mode.

Manager API Hooks

UI addons rely on specific hooks defined in storybook/manager-api to interact with Storybook state and communicate with the preview iframe.

Hook Description
useGlobals Read and update global values accessible to all stories, such as theme toggles or viewport settings.
useStorybookApi Access the full Storybook API including setAddonShortcut, getCurrentStoryData, and navigation methods.
useAddonState Persist local UI state across the manager's mount-unmount cycles without affecting story globals.
useChannel Emit and listen to custom events on the channel connecting manager and preview iframes.

Step-by-Step: Creating a Toolbar Addon

The following implementation demonstrates how to create custom Storybook addons that add interactive toolbar buttons using globals and keyboard shortcuts.

The Registration Entry Point

Create a manager entry file that registers your addon and defines its toolbar placement. This pattern mirrors the implementation in code/addons/pseudo-states/src/manager.ts:

// src/manager.ts
import { addons, types } from 'storybook/manager-api';
import { Tool } from './Tool';

const ADDON_ID = 'my-custom-addon';
const TOOL_ID = `${ADDON_ID}/tool`;

addons.register(ADDON_ID, () => {
  addons.add(TOOL_ID, {
    type: types.TOOL,
    title: 'My Custom Tool',
    match: ({ viewMode }) => viewMode === 'story',
    render: Tool,
  });
});

Building the React Component

The toolbar component uses useGlobals to toggle state and useStorybookApi to register keyboard shortcuts. This example follows the patterns found in code/core/src/viewport/components/Tool.tsx:

// src/Tool.tsx
import React, { useEffect } from 'react';
import { useGlobals, useStorybookApi } from 'storybook/manager-api';
import { Button } from 'storybook/internal/components';
import { LightningIcon } from '@storybook/icons';

const ADDON_ID = 'my-custom-addon';
const PARAM_KEY = `${ADDON_ID}/active`;

export const Tool = () => {
  const [globals, updateGlobals] = useGlobals();
  const api = useStorybookApi();

  const isActive = [true, 'true'].includes(globals[PARAM_KEY]);

  const toggle = () => updateGlobals({ [PARAM_KEY]: !isActive });

  useEffect(() => {
    api.setAddonShortcut(ADDON_ID, {
      label: 'Toggle My Addon [Alt+8]',
      defaultShortcut: ['Alt-8'],
      actionName: 'toggleAddon',
      showInMenu: false,
      action: toggle,
    });
  }, [toggle, api]);

  return (
    <Button
      active={isActive}
      aria-label="Enable My Addon"
      onClick={toggle}
      variant="ghost"
      padding="small"
    >
      <LightningIcon />
    </Button>
  );
};

Essential Hooks for Custom Storybook Addons

Beyond the basic toolbar implementation, advanced addons require state persistence and cross-frame communication.

Persisting Local State with useAddonState

When you need to maintain UI state that should not be exposed as story globals, use useAddonState as shown in docs/addons/addons-api.mdx:

import { useAddonState } from 'storybook/manager-api';

export const Tool = () => {
  const [state, setState] = useAddonState('my-addon-state', { enabled: false });
  
  const toggle = () => setState({ enabled: !state.enabled });
  
  return (
    <Button active={state.enabled} onClick={toggle}>
      Toggle Feature
    </Button>
  );
};

Cross-Frame Communication with useChannel

To send data from the manager to the preview iframe, establish a channel connection:

import { useChannel } from 'storybook/manager-api';

export const Tool = () => {
  const emit = useChannel({
    MY_CUSTOM_EVENT: (payload) => console.log('Received from preview:', payload),
  });

  const handleClick = () => emit('MY_CUSTOM_EVENT', { timestamp: Date.now() });

  return <Button onClick={handleClick}>Send to Preview</Button>;
};

Bundling and Build Configuration

Custom Storybook addons require specific build outputs to function in both the manager and preview environments. The official Addon Kit uses tsup (a zero-config esbuild wrapper) to handle this automatically.

The standard tsup.config.ts emits three distinct bundles:

  • dist/manager.mjs for UI code running in the manager
  • dist/preview.mjs for code injected into the preview iframe
  • dist/index.js for Node-side preset execution

You can customize bundling behavior via the bundler field in package.json or by modifying the tsup configuration directly. The build process is documented in docs/addons/writing-addons.mdx and demonstrated in the Addon Kit repository.

Publishing Your Custom Addon

When preparing to publish your addon to npm, your package.json must include specific exports and Storybook metadata to ensure proper integration:

{
  "exports": {
    ".": { 
      "types": "./dist/index.d.ts", 
      "node": "./dist/index.js", 
      "import": "./dist/index.mjs" 
    },
    "./manager": "./dist/manager.mjs",
    "./preview": "./dist/preview.mjs"
  },
  "storybook": {
    "displayName": "My Storybook Addon",
    "icon": "https://example.com/icon.png"
  }
}

For Node-side configuration, create a preset file that Storybook loads during initialization:

// src/preset.ts
module.exports = {
  managerWebpack: async (config) => {
    // Modify webpack config for the manager UI
    return config;
  },
  previewAnnotations: async (annotations) => {
    // Add decorators or parameters to the preview
    return [...annotations, require.resolve('./preview.js')];
  },
};

Reference this preset in your package.json so consumers can install your addon without manual configuration:

{
  "storybook": {
    "presets": ["./dist/preset"]
  }
}

Summary

  • Storybook addons extend the manager UI or preview iframe using storybook/manager-api and storybook/preview-api respectively.
  • Registration occurs via addons.register and addons.add in a dedicated manager entry file, specifying UI types like types.TOOL, types.PANEL, or types.TAB.
  • Core hooks including useGlobals, useAddonState, and useChannel enable state management and communication between manager and preview.
  • Build configuration uses tsup to output separate bundles for manager (dist/manager.mjs), preview (dist/preview.mjs), and Node (dist/index.js) environments.
  • Publishing requires specific package.json exports and a storybook metadata field to appear in the Storybook Integrations Catalog.

Frequently Asked Questions

What is the difference between manager and preview in Storybook addons?

The manager is the outer Storybook application shell containing the navigation sidebar, toolbar, and addon panels, while the preview is the isolated iframe that renders your actual components. Addons targeting the manager use storybook/manager-api to render UI elements and handle user interactions, whereas addons affecting story rendering use storybook/preview-api to inject decorators, modify parameters, or transform the story output before it displays.

When should I use useGlobals versus useAddonState?

Use useGlobals when your addon state needs to be accessible to stories themselves, such as toggling a theme or changing a viewport size that components react to. Use useAddonState when the state is purely internal to your addon's UI, such as tracking whether a panel is expanded or collapsed, where persistence across remounts is needed but story components should not be aware of the value.

How do I communicate between my addon's manager code and the preview iframe?

Use the useChannel hook from storybook/manager-api to establish a communication bridge. The hook returns an emit function to send events to the preview and accepts a handlers object to receive events from the preview. This channel uses Storybook's internal event system to pass data across the iframe boundary, enabling your addon to coordinate state between the toolbar UI and story decorators.

What files must I include when publishing a custom Storybook addon?

Your published package must include dist/manager.mjs for UI code, dist/preview.mjs for iframe modifications, and optionally dist/index.js for Node preset logic. Your package.json must export these paths explicitly under the exports field and include a storybook metadata object with displayName and icon properties for catalog integration. The Addon Kit provides a complete tsup configuration and release tooling via Auto to handle these requirements automatically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →