Storybook, components in isolation and tests
Storybook is an environment where you run a user interface component on its own, outside the application, in a dozen states at once. The storybookjs/storybook repository holds roughly 91 thousand stars, the licence is MIT, and the current version is 10.5.10. This text covers what you get in return for a configuration you then have to maintain.
What Storybook actually does
Three different tools hide under one name, and separating them is the first thing to do before counting costs.
The first is a working environment. Instead of clicking through the application to reach a form in its error state, you open that state directly from the list on the left of the screen. Every variant of a component is a separate entry with an addressable link you can send to a tester or a designer. For components buried deep in a flow, a payment screen three form steps in for instance, this is the largest time saving in the whole package.
The second is documentation. Storybook can generate a page describing a component from the types of its properties and the set of prepared variants. You get a property table with types and default values, live examples, and the ability to change arguments in the browser. For a team whose components are consumed by someone outside the authoring team, this is often the only documentation anybody reads.
The third is the testing layer. To a component variant you add a function that clicks, types text, and checks the result. One definition then serves three purposes at once: preview, documentation, and test. That is the real argument for the tool, because a single artefact pays back three times.
What Storybook does not do matters equally. It does not replace application level tests, since a component running in isolation sees no routing, no session, and no real backend. It is not a design system, only a place where a design system can be shown. Nor does it render global state from React or from a data store by itself: context that the application supplies at the root of the tree has to be supplied separately in the preview configuration.
Installation and what actually lands in the project
Installation is a single command, but what remains in the dependency directory afterwards is worth inspecting before deciding.
# detects the framework and creates the .storybook directory
npx storybook init
# local run and building the static version
npm run storybook
npm run build-storybook
# maintenance commands added by init
npx storybook upgrade
npx storybook automigrate
npx storybook doctorVersion ten, released on 28 October 2025, brought one compatibility breaking change: published code is ES modules only. That requires Node 20.16 or newer within the 20 line, 22.19 or newer within the 22 line, or anything from the 24 line. A build server with an older pinned Node stops immediately, before it reaches any configuration at all.
Dropping CommonJS cut the install size by 29 percent against version nine. That is a real improvement, but the starting point was high and remains high. The storybook package alone occupies 20.3 megabytes unpacked across 233 files and pulls in seventeen direct dependencies. Among them sit @vitest/spy and @vitest/expect pinned at 3.2.4, @testing-library/dom, @testing-library/user-event, esbuild, oxc-parser, and oxc-resolver. Storybook therefore carries a sizeable slice of testing infrastructure inside itself before you add a single addon.
The licence is MIT and three sources agree on it: the LICENSE file in the repository, the license field in the npm registry, and the metadata served by GitHub's programming interface. There is one detail that can surprise during an audit, though: the published package contains no licence text file at all. The 10.5.10 archive holds only README.md, package.json, a dist directory, and an assets directory. Dependency scanners that read package contents rather than registry metadata will report a gap here, even though the licence itself is not in doubt.
Configuration lives in the .storybook directory, with main.ts as its core.
import type { StorybookConfig } from '@storybook/react-vite'
const config: StorybookConfig = {
framework: {
name: '@storybook/react-vite',
options: {}
},
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'],
addons: [
'@storybook/addon-docs',
'@storybook/addon-a11y',
'@storybook/addon-vitest'
],
staticDirs: ['../public'],
core: {
disableTelemetry: true
},
typescript: {
reactDocgen: 'react-docgen-typescript'
}
}
export default configThe core.disableTelemetry field is not there by accident. Storybook sends anonymous usage data by default: commands invoked, version, framework, addon list, variant count, package manager. Three routes switch it off, the configuration field shown above, the --disable-telemetry flag, or the STORYBOOK_DISABLE_TELEMETRY=true variable. Order matters, because a boot event is sent before the configuration file loads, so only the environment variable disables it completely.
What a story looks like
A component variant is described in Component Story Format, which simply means an exported object in a file next to the component.
import type { Meta, StoryObj } from '@storybook/react-vite'
import { expect, fn } from 'storybook/test'
import { PaymentForm } from './PaymentForm'
const meta = {
component: PaymentForm,
tags: ['autodocs'],
args: {
onSubmit: fn(),
currency: 'PLN'
},
argTypes: {
currency: { control: 'select', options: ['PLN', 'EUR', 'USD'] }
}
} satisfies Meta<typeof PaymentForm>
export default meta
type Story = StoryObj<typeof meta>
export const Default: Story = {}
export const InsufficientFunds: Story = {
args: { balance: 0 },
tags: ['!autodocs']
}
export const Submitted: Story = {
play: async ({ args, canvas, step, userEvent }) => {
await step('Fill in the amount', async () => {
await userEvent.type(canvas.getByLabelText('Amount'), '250')
})
await step('Confirm', async () => {
await userEvent.click(canvas.getByRole('button', { name: 'Pay' }))
})
await expect(args.onSubmit).toHaveBeenCalledWith({
amount: 250,
currency: 'PLN'
})
}
}Several things in that example need a comment. The fn function from the storybook/test module creates a spy you can later assert calls against, while also logging them in the actions panel. The canvas argument passed into the play function is the render area of that particular variant rather than the whole document, so queries do not catch elements from other variants. The step function groups steps in the interactions panel, and it decides how readable the report from a failed test will be.
Tags work as an inheritance system from the preview level, through component metadata, down to a single variant. Three of them, dev, manifest, and test, are applied to every variant automatically. The autodocs tag, which switches on the generated documentation page, has to be added yourself. The exclamation mark form, like !autodocs in the example, removes a tag inherited from above, which helps with variants describing an error state you do not want in the documentation.
Interaction tests and the Vitest integration
This is the part that changed the most across recent releases, and the part that decides whether Storybook is a cost or an investment in your project.
The @storybook/addon-vitest addon turns variants into tests run by Vitest. This is not a separate mechanism running alongside your tests: it is a Vitest plugin that pushes variant files through the same pipeline as the rest of the suite. The default configuration runs them in browser mode, in Chromium driven by Playwright, rather than in a simulated environment such as jsdom.
npx storybook add @storybook/addon-vitest
npx playwright install chromium
npx vitest --project storybookThe Vitest side of the configuration looks like the following, and this is where things most often drift apart.
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { defineConfig, mergeConfig } from 'vitest/config'
import { playwright } from '@vitest/browser-playwright'
import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'
import viteConfig from './vite.config'
const dirname = path.dirname(fileURLToPath(import.meta.url))
export default mergeConfig(
viteConfig,
defineConfig({
test: {
projects: [
{
extends: true,
plugins: [
storybookTest({
configDir: path.join(dirname, '.storybook'),
storybookScript: 'npm run storybook -- --no-open',
tags: { include: ['test'], exclude: ['experimental'], skip: [] }
})
],
test: {
name: 'storybook',
browser: {
enabled: true,
provider: playwright({}),
headless: true,
instances: [{ browser: 'chromium' }]
},
setupFiles: ['./.storybook/vitest.setup.ts']
}
}
]
}
})
)The storybookTest plugin accepts configDir pointing at the configuration directory, storybookScript with the command that runs Storybook in watch mode, storybookUrl defaulting to http://localhost:6006, a tags object with include, exclude, and skip lists, and disableAddonDocs, which defaults to true and skips MDX processing during tests. The three tag lists differ in effect: excluded variants disappear from the report entirely, while skipped ones stay visible in it as not executed.
The setup file is short, but without it variants render without the decorators from the preview file and tests fall over on missing context.
import { beforeAll } from 'vitest'
import { setProjectAnnotations } from '@storybook/react-vite'
import * as previewAnnotations from './preview'
const annotations = setProjectAnnotations([previewAnnotations])
beforeAll(annotations.beforeAll)There is one hard limitation here, better known before you start. The addon works only with Storybook frameworks built on Vite: react-vite, vue3-vite, svelte-vite, preact-vite, sveltekit, or nextjs-vite. A project on @storybook/nextjs, the webpack variant, requires moving to @storybook/nextjs-vite first, and that is not always a neutral change. Vitest at version three or above is also required, and version four changed how the browser is declared, moving it into a separate @vitest/browser-playwright package, so examples from earlier material do not carry over one to one.
The price of that convenience is browser binaries on the build server. The Chromium build fetched by Playwright weighs several hundred megabytes and has to be either installed on every run or kept in an image cache. With a pipeline triggered dozens of times a day, that is a line item you see on the machine time bill.
Accessibility, documentation, and module mocking
Three addons decide whether Storybook stays in a project long term or turns into a gallery nobody opens.
The @storybook/addon-a11y addon runs the axe engine against every variant. You control it through the a11y.test parameter, which takes three values. Setting off disables checking, todo reports violations as warnings, and error turns them into a test failure. The middle value is, against expectation, the most important one, because it lets you switch checking on in a project that already has a backlog without halting the whole pipeline on day one. The a11y.config and a11y.options fields go straight into the axe.configure and axe.run calls, so narrowing the rule set to the guidelines that actually bind you is a matter of one object.
Module mocking in version ten has its own mechanism and, importantly, one place where you may use it.
import type { Preview } from '@storybook/react-vite'
import { sb } from 'storybook/test'
sb.mock(import('../src/lib/session.ts'), { spy: true })
sb.mock(import('uuid'))
const preview: Preview = {
parameters: {
a11y: {
test: 'todo',
options: { runOnly: ['wcag2a', 'wcag2aa'] }
},
controls: {
matchers: { color: /(background|color)$/i }
}
},
tags: ['autodocs']
}
export default previewThe sb.mock function takes an import expression rather than a path string, and must be called in the .storybook/preview.* file. The spy: true option preserves the module's original behaviour and merely wraps it in a spy, which suits modules whose behaviour you want to observe rather than replace. A call without options swaps every export for empty spies, the right choice for identifier or date generators that break visual comparisons.
Automatically generated documentation draws on two sources. The property table comes from type analysis, driven by the typescript.reactDocgen field in the configuration file. Descriptions and prose sections you write in MDX files, pulled in by a pattern in the stories field. That second part is the one that ages fastest, because nothing forces it to be updated when a component changes.
The real maintenance cost and when it stops paying off
This is the heart of the matter, because this question decides the success of an adoption more often than any feature listed above.
Storybook is a second build pipeline in your project. It has its own configuration file, its own set of addons, its own version, and its own Node requirements. A change to the Tailwind CSS configuration, to path aliases, or to how fonts are loaded has to be mirrored in both places. When that mirroring lapses, components look different in Storybook than in the application, the tool stops being trustworthy, and within weeks nobody opens it.
The release cadence counts too. Version nine appeared on 28 May 2025, version ten five months later, and patch releases arrive practically weekly: 10.5.10 was published on 20 August 2026. The project is alive, which is a strength, but every major version means auditing the addons, some of which lag behind. The storybook upgrade and storybook automigrate commands carry a large share of the changes automatically, yet an addon maintained by one person can stall an entire migration for weeks.
The third cost concerns visual tests. Storybook does not compare snapshots by itself; you need a service that stores history and shows differences. The nearest choice is Chromatic, run by the same company, and its pricing deserves a look before you promise anything to the team.
| Chromatic plan | Monthly price | Billed snapshots | Browsers |
|---|---|---|---|
| Free | 0 USD | 5,000 | Chrome |
| Starter | 179 USD | 35,000 | Chrome, Safari, Firefox, Edge |
| Pro | 399 USD | 85,000 | Chrome, Safari, Firefox, Edge |
| Enterprise | custom quote | unlimited | Chrome, Safari, Firefox, Edge |
Snapshots above the Starter allowance cost 0.008 USD each. The bill grows as a product: variant count times browser count times change request count. A hundred variants checked across four browsers on twenty pull requests a day is eight thousand snapshots a day, which puts you past the free plan after the first working day. The TurboSnap mechanism limits this by copying snapshots of unchanged components instead of retaking them, but it depends on correct detection of dependencies between files, and in a monorepo that becomes a separate source of trouble.
So when does Storybook stop paying off. With a two person team building one application, where there are a dozen or so components, each used once, and nobody outside the team ever looks at them, the second configuration costs more than it gives. You reach the same result by running component tests directly in Vitest browser mode, with no .storybook directory and no interface to maintain.
The same holds when components are tightly fused with the application. If each of them reaches for data, session, and routing, preparing it to render in isolation means so many spies and stubs that the variant file becomes a second implementation of the component and starts lying on every change.
It does pay off when components have more than one consumer. A library shared across applications, a team with a separate design role, a product with an accessibility requirement written into a public tender, a migration of styling to another system: in each of these, Storybook is a cheap answer to the question "what does this look like right now". The same goes for work with ready made component sets such as shadcn/ui or Chakra UI, where a catalogue of variants quickly shows what your own theme broke.
Storybook against the alternatives
Competition exists, but it is visibly smaller and narrower, which is itself a piece of information.
| Tool | Current version | Last release | Stars | Frameworks | Licence |
|---|---|---|---|---|---|
| Storybook | 10.5.10 | August 2026 | approx. 91k | React, Vue, Svelte, Angular, Web Components, Next.js | MIT |
| React Cosmos | 7.4.0 | August 2026 | approx. 8.7k | React, React Native | MIT |
| Histoire | 1.0.0-beta.1 | January 2026 | approx. 3.6k | Vue, Svelte | MIT |
| Ladle | 5.1.1 | November 2025 | approx. 3k | React | MIT |
The conclusions from that table are fairly plain. Ladle is lighter and starts faster, but supports React only, and its last release is nine months old. Histoire has sat on a version marked as a pre-release since January 2026, which is hard to justify for a tool wired into a build pipeline. React Cosmos is actively developed and holds a sensible niche in React Native, but its addon ecosystem does not compare with Storybook's.
A fifth option is no tool at all. Vitest in browser mode runs component tests without any of these layers, and the preview page can be replaced by a directory of routes inside the application itself. You lose documentation and browsing convenience, you gain one configuration instead of two. For a small team that is often the better balance.
Common mistakes
The first is treating variants as unit tests for logic. The play function launches a real browser and renders the component, so checking a pure date formatting function inside it costs hundreds of times more time than an ordinary test. Variants should cover behaviour visible to the user; leave the rest to the normal test suite.
The second is drift between the application configuration and the Storybook configuration. Path aliases, environment variables, and the styling layer have to be shared. In a Vite project the simplest route is pulling the existing configuration in through viteFinal in main.ts, rather than maintaining two plugin lists that diverge after the first month.
The third is skipping the setup file with the Vitest addon. Without a setProjectAnnotations call, variants render without the decorators from the preview file, so tests fail on a missing theme or context provider, and the message points at a component that is entirely innocent.
The fourth is publishing a built Storybook without thinking. The storybook-static directory contains full component code, comments, programming interface addresses from preview files, and everything that went into the spies. For an internal administration system that amounts to publishing a map of its features.
The fifth is growing variants without pruning. A variant file is code that ages: the component changes its properties while the variants stay in a six month old shape and render with default values nobody intended. Running variants as tests in the pipeline solves this on its own, because a dead variant stops compiling and forces a decision.
The sixth is installing an addon for every need. Every addon is code loaded when the interface starts and another item to check on a major version change. Three addons that genuinely change how the team works are worth more than twelve, half of which will stall the next migration.
FAQ
Does Storybook replace unit and end-to-end tests?
It replaces neither. Interaction tests check one component in isolation, without routing, session, or a real backend. Logic without an interface still needs ordinary tests, and flows across the whole application still need an end-to-end tool.
Does the Vitest addon work with Next.js?
It does, but only through the @storybook/nextjs-vite framework and with Next.js at version 14.1 or above. The webpack variant @storybook/nextjs does not meet the requirement, because the addon is a Vite plugin. Moving between those frameworks needs to be planned separately.
What do visual tests cost to run?
Storybook itself costs nothing, since it does not compare snapshots. An external service does: the Chromatic free plan covers 5 thousand billed snapshots a month and Chrome only, the Starter plan costs 179 USD a month for 35 thousand snapshots, and Pro costs 399 USD for 85 thousand. Snapshots above the Starter allowance are 0.008 USD each.
Does Storybook send data about my project?
By default yes, as anonymous telemetry covering version, framework, addon list, and variant count. You disable it with the core.disableTelemetry field, the --disable-telemetry flag, or the STORYBOOK_DISABLE_TELEMETRY variable. Only that last route also stops the boot event, which is sent before configuration loads.
Which Node version does version ten require?
Node 20.16 or newer within the 20 line, 22.19 or newer within the 22 line, or anything from the 24 line. It follows from the move to ES modules only, which needs support for loading such modules through require.
Can I use Storybook without writing separate variant files?
Not in any sensible way. The variant is the base unit here, and everything, documentation, tests, visual review, derives from it. If writing those files feels like an unacceptable cost, that is a signal the tool will not repay itself in this project.
Documentation lives on the Storybook site, the source code in the GitHub repository, and visual testing prices on the Chromatic site.