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.registerserves as the entry point for every custom Storybook addon.addons.adddescribes the UI element, including itstype(types.TOOL,types.PANEL, ortypes.TAB), display title, and React component.- The
matchfunction 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.mjsfor UI code running in the managerdist/preview.mjsfor code injected into the preview iframedist/index.jsfor 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-apiandstorybook/preview-apirespectively. - Registration occurs via
addons.registerandaddons.addin a dedicated manager entry file, specifying UI types liketypes.TOOL,types.PANEL, ortypes.TAB. - Core hooks including
useGlobals,useAddonState, anduseChannelenable 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.jsonexports and astorybookmetadata 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →