Addon migration guide for Storybook 10.0
We sincerely appreciate the dedication and effort addon creators put into keeping the Storybook ecosystem vibrant and up-to-date. As Storybook evolves to version 10.0, bringing new features and improvements, this guide is here to assist you in migrating your addons from 9.x to 10.0. If you need to migrate your addon from an earlier version of Storybook, please first refer to the Addon migration guide for Storybook 9.0.
We also have a general Storybook migration guide that covers updates to your Storybook instance rather than your addon code.
Dependency updates
You will need to update your Storybook dependencies. Peer dependencies must point to ^10.0.0
to ensure broad compatibility for your end users. Development dependencies can be set to ^10.0.0
, or to next
if you want to try the latest prerelease all year round.
{
"devDependencies": {
"@storybook/addon-docs": "next",
"@storybook/react-vite": "next",
"storybook": "next"
},
"peerDependencies": {
"storybook": "^10.0.0"
}
}
If you still have @storybook/addon-essentials
, @storybook/addon-interactions
, @storybook/addon-links
, or @storybook/blocks
in your dependencies, you will need to remove them. These packages are empty since Storybook 9 and will no longer be published going forward.
Supporting earlier versions
If your addon supports multiple major versions of Storybook, you can specify a wider version range in your peer dependencies:
{
"name": "your-storybook-addon",
"peerDependencies": {
"storybook": "^9.0.0 || ^10.0.0"
},
"devDependencies": {
"storybook": ">=10.0.0-0 <11.0.0-0" // For local development
}
}
However, we recommend releasing a new major version of your addon alongside new major versions of Storybook. This practice:
- Makes it easier to maintain your code
- Allows you to take advantage of new features and improvements
- Provides a clearer upgrade path for your users
Key changes for addons
Here are the changes in version 10.0 that impact addon development.
ESM-only builds
Storybook 10 requires all addons to be built as ESM-only. This change simplifies the build process and reduces maintenance overhead. You'll need to make many changes to tsup.config.ts
, so it can be easier to copy the reference file in the addon-kit
repository.
This update brings the following changes:
- The Node target moves from Node 20.0 to Node 20.19
- You no longer need to build CJS files
- You no longer need to pass
globalManagerPackages
andglobalPreviewPackages
- The
bundler
config inpackage.json
no longer needs to be manually typed - We recommend you stop using
exportEntries
and switch exported entries topreviewEntries
andmanagerEntries
instead based on where they are consumed by your users
- import { readFile } from "node:fs/promises";
import { defineConfig, type Options } from "tsup";
- import { globalPackages as globalManagerPackages } from "storybook/internal/manager/globals";
- import { globalPackages as globalPreviewPackages } from "storybook/internal/preview/globals";
- const NODE_TARGET: Options["target"] = "node20";
+ const NODE_TARGET = "node20.19"; // Minimum Node version supported by Storybook 10
- type BundlerConfig = {
- bundler?: {
- exportEntries?: string[];
- nodeEntries?: string[];
- managerEntries?: string[];
- previewEntries?: string[];
- };
- };
export default defineConfig(async (options) => {
// reading the three types of entries from package.json, which has the following structure:
// {
// ...
// "bundler": {
- // "exportEntries": ["./src/index.ts"],
// "managerEntries": ["./src/manager.ts"],
- // "previewEntries": ["./src/preview.ts"],
+ // "previewEntries": ["./src/preview.ts", "./src/index.ts"],
// "nodeEntries": ["./src/preset.ts"]
// }
// }
- const packageJson = (await readFile("./package.json", "utf8").then(
- JSON.parse,
- )) as BundlerConfig;
+ const packageJson = (
+ await import("./package.json", { with: { type: "json" } })
+ ).default;
+
const {
bundler: {
- exportEntries = [],
managerEntries = [],
previewEntries = [],
nodeEntries = [],
- } = {},
+ },
} = packageJson;
const commonConfig: Options = {
- splitting: false,
+ splitting: true,
+ format: ["esm"],
- minify: !options.watch,
treeshake: true,
- sourcemap: true,
// keep this line commented until https://github.com/egoist/tsup/issues/1270 is resolved
// clean: options.watch ? false : true,
clean: false,
+ // The following packages are provided by Storybook and should always be externalized
+ // Meaning they shouldn't be bundled with the addon, and they shouldn't be regular dependencies either
+ external: ["react", "react-dom", "@storybook/icons"],
};
const configs: Options[] = [];
-
- // export entries are entries meant to be manually imported by the user
- // they are not meant to be loaded by the manager or preview
- // they'll be usable in both node and browser environments, depending on which features and modules they depend on
- if (exportEntries.length) {
- configs.push({
- ...commonConfig,
- entry: exportEntries,
- dts: {
- resolve: true,
- },
- format: ["esm", "cjs"],
- platform: "neutral",
- target: NODE_TARGET,
- external: [...globalManagerPackages, ...globalPreviewPackages],
- });
- }
// manager entries are entries meant to be loaded into the manager UI
// they'll have manager-specific packages externalized and they won't be usable in node
// they won't have types generated for them as they're usually loaded automatically by Storybook
if (managerEntries.length) {
configs.push({
...commonConfig,
entry: managerEntries,
- format: ["esm"],
platform: "browser",
- target: BROWSER_TARGETS,
+ target: "esnext", // we can use esnext for manager entries since Storybook will bundle the addon's manager entries again anyway
- external: globalManagerPackages,
});
}
// preview entries are entries meant to be loaded into the preview iframe
// they'll have preview-specific packages externalized and they won't be usable in node
// they'll have types generated for them so they can be imported when setting up Portable Stories
if (previewEntries.length) {
configs.push({
...commonConfig,
entry: previewEntries,
- dts: {
- resolve: true,
- },
- format: ["esm", "cjs"],
platform: "browser",
- target: BROWSER_TARGETS,
+ target: "esnext", // we can use esnext for preview entries since Storybook will bundle the addon's preview entries again anyway
- external: globalPreviewPackages,
+ dts: true,
});
}
// node entries are entries meant to be used in node-only
// this is useful for presets, which are loaded by Storybook when setting up configurations
// they won't have types generated for them as they're usually loaded automatically by Storybook
if (nodeEntries.length) {
configs.push({
...commonConfig,
entry: nodeEntries,
- format: ["cjs"],
platform: "node",
target: NODE_TARGET,
});
Next, update the exports
field in your package.json
to remove mentions of CJS files.
"exports": {
".": {
"types": "./dist/index.d.ts",
- "import": "./dist/index.js",
- "require": "./dist/index.cjs"
+ "default": "./dist/index.js"
},
"./preview": {
- "types": "./dist/index.d.ts",
- "import": "./dist/preview.js",
- "require": "./dist/preview.cjs"
+ "types": "./dist/preview.d.ts",
+ "default": "./dist/preview.js"
},
- "./preset": "./dist/preset.cjs",
+ "./preset": "./dist/preset.js",
"./manager": "./dist/manager.js",
"./package.json": "./package.json"
},
Update tsconfig.json
to use esnext
modules.
{
"compilerOptions": {
// ...
- "module": "preserve",
+ "module": "esnext",
// ...
- "lib": ["es2023", "dom", "dom.iterable"],
+ "lib": ["esnext", "dom", "dom.iterable"],
},
- "include": ["src/**/*"]
+ "include": ["src/**/*", "tsup.config.ts"]
}
Finally, change the preset.js
file at the top-level of your addon to be ESM. This file used to be CJS because the Storybook Node app only supported CJS.
-// this file is slightly misleading. It needs to be CJS, and thus in this "type": "module" package it should be named preset.cjs
-// but Storybook won't pick that filename up so we have to name it preset.js instead
-
-module.exports = require('./dist/preset.cjs');
+export * from './dist/preset.js';
Local addon loading
Because addons are now ESM-only, you must change how you load your own addon in your development Storybook instance. Imports and exports must now follow ESM syntax, and relative paths must use import.meta.resolve
.
Remove .storybook/local-preset.cjs
and create .storybook/local-preset.ts
with the following content:
import { fileURLToPath } from "node:url";
export function previewAnnotations(entry = []) {
return [...entry, fileURLToPath(import.meta.resolve("../dist/preview.js"))];
}
export function managerEntries(entry = []) {
return [...entry, fileURLToPath(import.meta.resolve("../dist/manager.js"))];
}
export * from "../dist/preset.js";
Next, update your main.ts
to reference the new preset file:
- addons: ["@storybook/addon-docs", "./local-preset.cjs"],
+ addons: ["@storybook/addon-docs", import.meta.resolve("./local-preset.ts")],
Optional changes
The following changes are not strictly required yet, but we recommend making them to improve your users' experience.
CSF Factories support
To support CSF Factories annotations in your addon, you will need to update your src/index.ts
file to use the new definePreviewAddon
. This feature will be part of CSF Next. This change is highly recommended, as it will help your own users reap the benefits of CSF Factories.
With CSF Factories, users can chain their preview configuration and benefit from better typing and more flexibility. Addons must export annotations to be compatible with this new syntax. CSF Factories will be the default way to write stories in Storybook 11.
- // make it work with --isolatedModules
- export default {};
+ import { definePreviewAddon } from "storybook/internal/csf";
+ import addonAnnotations from "./preview";
+
+ export default () => definePreviewAddon(addonAnnotations);
Removal of exportEntries
The exportEntries
property in package.json
's bundler
was used to produce the index.js
build output from src/index.ts
. It was compatible with Node.js, rather than strictly with browsers. This build configuration could cause subtle bugs when addon authors exported code in index.js
for use in the Storybook preview or manager.
In the Storybook 10 addon-kit, we removed exportEntries
from the bundler
config, and we moved src/index.ts
to be part of previewEntries
instead. This way, any code exported from src/index.ts
is bundled for browsers and usable with CSF Next. If you need to export additional code to run in the preview (such as optional decorators), you can add them to src/index.ts
.
If you need to export code for the manager (such as a renderLabel
function for the sidebar), you can create a new src/manager-helpers.ts
file and add it to managerEntries
, like so:
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./preview": {
"types": "./dist/preview.d.ts",
"default": "./dist/preview.js"
},
"./preset": "./dist/preset.js",
"./manager": "./dist/manager.js",
+ "./manager-helpers": "./dist/manager-helpers.js",
"./package.json": "./package.json"
},
"bundler": {
"managerEntries": [
+ "src/manager-helpers.ts",
"src/manager.tsx"
]
}
Build file cleanup
We recommend removing your old build files as you build. This will avoid your dist
folder growing with expired JS chunks. You may add the following to your package.json
scripts:
{
"scripts": {
+ "prebuild": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
}
}
Package keyword changes
We've updated default keywords for addons in the Storybook addon-kit
template.
"keywords": [
- "storybook-addons",
+ "storybook-addon",
],
10.0.0 full migration guide
For a full list of changes, please visit the Migration.md file
Migration example
For a complete example of an addon updated to support Storybook 10.0, refer to the Addon Kit migration PR. Once merged, it will demonstrate all the necessary and recommended changes for Storybook 10.
Releasing
To support Storybook 10.0, we encourage you to release a new major version of your addon. For experimental features or testing, use the next
tag. This allows you to gather feedback before releasing a stable version.
Support
If you're having issues with your addon after following this guide, please open a new discussion in our GitHub repository or come talk to us in our dedicated addon developer channel, #addons
on Discord.