Migrating From @storybook/ember
The official @storybook/ember was build for Ember v3 for classic build (with webpack). Storybook v10 makes vite the default renderer, which @storybook/ember does not support, making it incompatible. Together with the Storybook we discussed the future for this project and decided to move it into ember-storybook. The two packages support two generations of ember and storybook (see the table below).
This guide helps you step-by-step to migrate from @storyboo/ember to ember-storybook.
@storybook/ember | ember-storybook |
|---|---|
| Ember v3 | Ember v6.8 |
| Storybook < 10 | Storybook v10 |
| Classic Build only | Vite |
| Requires an Ember app running | Spins up an App for you |
Step by Step
1. Swap the Packages
pnpm remove @storybook/ember && pnpm add -D storybook ember-storybooknpm uninstall @storybook/ember && npm install -D storybook ember-storybookyarn remove @storybook/ember && yarn add -D storybook ember-storybookbun remove @storybook/ember && bun add -d storybook ember-storybookStorybook 10 is required — run the upgrade first if you're on an older Storybook:
pnpm dlx storybook@latest upgradenpx storybook@latest upgradeyarn dlx storybook@latest upgradebun x storybook@latest upgrade2. Rewrite main.ts
Remove the legacy ember block (its configDir/scripts/styles keys have no equivalents), change the framework name, and keep stories:
import type { StorybookConfig } from 'ember-storybook';
const config: StorybookConfig = {
stories: ['../app/**/*.stories.g(j|t)s'],
framework: 'ember-storybook',
};
export default config;3. Point the Preview at Your App
The app entry you used to list under scripts becomes a factory under parameters.ember.app:
import { createApp } from '#app/app';
import { configure } from '#app/config';
import type { Preview } from 'ember-storybook';
const preview: Preview = {
parameters: {
ember: {
app: createApp,
configure,
},
},
};
export default preview;scripts: ['../app/app.js']→app(export acreateApp()from your app module that returns a non-autobooting instance; see Getting Started)configDir: 'config/environment.js'→ startup config code goes inconfigurestyles: ['../app/styles/app.css']→ import the stylesheet inpreview.ts; it applies to every story
4. Migrate Your Stories
CSF is CSF: title, named story exports, args, argTypes, decorators, parameters — none of that changes. What changes is how a story renders, and you can migrate file by file: add the new button.stories.gts alongside the legacy button.stories.ts — the stories glob in main.ts controls which set is live, so both can coexist until you delete the old ones.
This is the pattern from the Hokulea design system (migrated in #623), which migrated its stories side by side:
import { hbs } from 'ember-cli-htmlbars';
import { action } from 'storybook/actions';
export default {
title: 'Actions/Button',
component: 'button', // resolved from the app's registry by name
};
export const Showcase = {
render: (args) => ({
template: hbs`
<Button
@push={{this.push}}
@intent={{this.intent}}
@disabled={{this.disabled}}
>
{{this.label}}
</Button>
`,
context: {
...args,
disabled: parseOptionalBooleanArg(args.disabled),
push: action('button pushed'),
},
}),
args: {
label: 'Button',
},
};import { action } from 'storybook/actions';
import { Button } from './button.gts';
import type { Meta, StoryObj } from 'ember-storybook';
export default {
title: 'Actions/Button',
component: Button
} satisfies Meta;
export const Showcase: StoryObj = {
render: (args) => <template>
<Button
@push={{args.push}}
@intent={{args.intent}}
@disabled={{args.disabled}}
>
{{args.label}}
</Button>
</template>,
args: {
label: 'Button',
push: action('button pushed')
},
decorators: [(story, { args }) => story({ args: parseArgs(args) })]
};The mechanical rules:
- Rename the file to
.stories.gts— the template tag needs gjs/gts. - Import the component.
component: 'button'(a registry string, resolved by the running app) becomescomponent: Buttonwith an explicit import. - Drop
ember-cli-htmlbars. Therenderfunction that returned a{ template: hbs, context }object becomes an inline template:render: (args) => <template>…</template>, and{{this.foo}}references become{{args.foo}}. - Move
contextinto args or a decorator. Spies likeaction('button pushed')go straight intoargs; pre-computed values (icons, fixtures) become module-level consts; arg normalization (e.g. coercing the control panel's"true"string to a boolean) becomes a decorator that re-renders the story with parsed args. - Type your stories.
import type { Meta, StoryObj } from 'ember-storybook'replaces the untyped (or@storybook/ember-typed) exports —satisfies Metaon the default export,StoryObj<Args>on the stories. - Delete what's now free. Legacy
parameters.options.showPanelworkarounds and large hand-writtenargTypesoften fall away: argTypes and the docs signature are derived from component types (see Auto Docs).
5. Expect These Behavior Differences
- No router. The old app-served model had your app's router alive; here nothing boots it (see
configure). For template-level{{outlet}}use route stories, for URL-driven code pass args in. - Services are real. If a service hits the network, it hits the network — add MSW or stub via
owner.