# How to Create Custom Storybook Addons: A Complete Developer Guide

> Learn to create custom Storybook addons with this complete guide. Extend Storybook UI and iframe functionality using manager and preview APIs for powerful integrations.

- Repository: [Storybook/storybook](https://github.com/storybookjs/storybook)
- Tags: how-to-guide
- Published: 2026-02-27

---

**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`](https://github.com/storybookjs/storybook/blob/main/code/addons/pseudo-states/src/manager.ts), the addon registers itself as a toolbar tool using `addons.register` and `addons.add`:

```typescript
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`](https://github.com/storybookjs/storybook/blob/main/code/addons/pseudo-states/src/manager.ts):

```typescript
// 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`](https://github.com/storybookjs/storybook/blob/main/code/core/src/viewport/components/Tool.tsx):

```typescript
// 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`:

```typescript
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:

```typescript
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`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/dist/index.js) for Node-side preset execution

You can customize bundling behavior via the `bundler` field in [`package.json`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/package.json) must include specific exports and Storybook metadata to ensure proper integration:

```json
{
  "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:

```typescript
// 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`](https://github.com/storybookjs/storybook/blob/main/package.json) so consumers can install your addon without manual configuration:

```json
{
  "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`](https://github.com/storybookjs/storybook/blob/main/dist/index.js)) environments.
- **Publishing** requires specific [`package.json`](https://github.com/storybookjs/storybook/blob/main/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`](https://github.com/storybookjs/storybook/blob/main/dist/index.js) for Node preset logic. Your [`package.json`](https://github.com/storybookjs/storybook/blob/main/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.