Upgrade Storybook and Vue in the same branch the library uses. A green Storybook on old Vue 3 beta stacks does not help when the app is on 3.5.x.
1. Stories for edge cases
Empty, loading, error, and “too much data” states. Those are the states that break when reactivity and slot forwarding change.
2. Addons that pay rent
a11y and interactions first; avoid addon sprawl. Your goal is faster feedback, not a second product to maintain.
3. Connect to the rest of the test stack
Chromatic or similar for visual diffs, or a lighter snapshot setup if budget is tight. The point is signal when class names and layout change, not only when JS throws.
4. A minimal Storybook 8 config for Vue 3
Storybook 8 with the Vite-Vue 3 framework is the path of least resistance. Keep .storybook/ small—your goal is fast feedback, not a configuration shrine. The two files below are enough to start.
// .storybook/main.ts
import type { StorybookConfig } from '@storybook/vue3-vite'
const config: StorybookConfig = {
stories: ['../src/**/*.stories.@(ts|js)'],
addons: [
'@storybook/addon-essentials',
'@storybook/addon-a11y',
'@storybook/addon-interactions'
],
framework: { name: '@storybook/vue3-vite', options: {} },
docs: { autodocs: 'tag' }
}
export default config// src/components/InvoiceRow.stories.ts
import type { Meta, StoryObj } from '@storybook/vue3'
import InvoiceRow from './InvoiceRow.vue'
const meta: Meta<typeof InvoiceRow> = {
component: InvoiceRow,
tags: ['autodocs'],
args: { invoice: { id: 'INV-1', amount: 1200, status: 'open' } }
}
export default meta
export const Default: StoryObj<typeof InvoiceRow> = {}
export const Overdue: StoryObj<typeof InvoiceRow> = {
args: { invoice: { id: 'INV-2', amount: 980, status: 'overdue' } }
}
export const Empty: StoryObj<typeof InvoiceRow> = {
args: { invoice: null }
}Each story is a contract: props in, visible state out. When you migrate InvoiceRow from Options API to script setup, those stories tell you—visually and via Volar—whether the contract held. Pair with our incremental TypeScript approach and the stories double as type-checked usage examples.
5. Stories as migration contracts
Treat each story as a behavior fence around a component. Before touching the source for Vue 3, capture five canonical stories: default, loading, error, empty, and overflow. Snapshot them. Migrate. Compare. The diff that matters is visual, not unit-test green.
- Default: the “happy path” the designer keeps in their head.
- Loading: spinners, skeletons, and the timing of when content swaps in.
- Error: network 5xx, validation failure, permission denied.
- Empty: no data, first-time-user, or filtered-to-zero.
- Overflow: 200-character names, 50-row tables, RTL locales—the states that break flex/grid in subtle ways.
If your component library covers those five for the top 30 components, the application-level Cypress component tests get to be much smaller and more focused on integration concerns.
6. Visual regression: where it pays off
Vue 3’s render function changes, scoped slot semantics, and class merging behaviors all shift the rendered HTML in ways that unit tests do not see. Visual regression on the Storybook URL is the cheapest insurance you can buy. Even a small set of stories run through Chromatic, Playwright screenshots, or a homegrown snapshot diff catches:
- Class merging differences (Vue 3 merges
classon root nodes differently from Vue 2 in some cases). - Scoped slot fallback content rendering when a slot is unused.
- Transition group keying changes that cause layout shifts.
- Vuetify/Element UI library upgrades that quietly bump padding by 2px everywhere.
Run visual regression on PRs that touch design tokens, on framework-version bumps, and after any third-party UI library upgrade. Don’t run it on every PR—snapshot fatigue kills adoption.
7. Anti-patterns we see
- Storybook as a documentation site only. If stories aren’t executed in CI, they rot. Wire them into the same pipeline as your unit tests.
- Mocking the entire backend in
decorators. A story should describe a component, not an integration. Push backend concerns to MSW or fixtures. - Addon hoarding. Each addon is a future migration burden.
essentials,a11y, andinteractionsare usually enough. - One mega-story with 40 controls. Stories are screenshots, not playgrounds. Keep them small and named.
- Storybook upgrade on a separate timeline. Storybook 8 in the same branch as Vue 3, please. A green Storybook on Vue 2 is theater.
8. FAQ
Do we need Storybook if we use Vue Test Utils?
They solve different problems. Vue Test Utils proves logic; Storybook proves visuals and UX states. Most teams need both during a migration—see our testing strategy guide.
Can we run Storybook on Vue 2 and Vue 3 at the same time?
Yes—two Storybook instances, one per branch of the codebase. It’s the cleanest way to compare components side by side during a strangler migration.
What about Histoire instead of Storybook?
Histoire is lighter and Vue-native. If your team has zero existing Storybook investment, it’s a reasonable choice. If you already run Storybook 8 in CI, the migration cost rarely justifies the switch.
Where does Storybook fit on the budget conversation?
It’s usually folded into the test infrastructure line of the migration cost estimate. Stories that already exist make the estimate go down.
9. Storybook safety-net checklist
- Storybook 8+ on the same Vue 3 version as the app.
- Top 30 components have default/loading/error/empty/overflow stories.
- Visual regression runs on framework bumps and design-token PRs.
- a11y addon flags WCAG violations and blocks PRs above a threshold.
- Stories deployed (Chromatic, Vercel, S3) so designers can review without checking out branches.
- Storybook build runs in the same CI as the app build—shared cache where possible.
- Stories live next to components, not in a separate
/storiesgraveyard.
A Storybook that fails this checklist isn’t a safety net—it’s a museum. Fund the gaps before you begin the next strangler vine.
Conclusion
Storybook does not replace E2E; it shrinks the E2E surface. Fund stories early in the program and the Vue 3 app integration phase gets boring—in a good way.
