Next.js
Storybook for Next.js & Rsbuild lets you develop and test Next.js components in isolation. Instead of asking you to configure Storybook separately, it reuses your app's own build setup — the same next.config.ts, compiler options, module aliases, and environment variables that next dev uses — so components behave in Storybook exactly as they do in your app.
There is no framework-specific build configuration to learn: keep configuring your app through next.config.ts, and Storybook picks it up as-is. The configuration mental model explains where any given setting belongs.
storybook-next-rsbuild relies on Next.js internals and moves with the Next.js release cadence. Any next minor or patch upgrade may break compatibility — pin next for reproducibility and upgrade storybook-next-rsbuild together with next.
Requirements
@rsbuild/core is a peer dependency rather than a pinned one because the right version depends on your next version — see the matrix below for the exact version to install.
Version matrix
Storybook's build and Next.js's build tooling must share a single copy of @rspack/core. Both @rsbuild/core and next-rspack pin an exact @rspack/core version, so the two pins have to agree — the framework checks this at startup and refuses to start when they differ, printing a link back to this table.
Pick the row that matches your next version and install the listed @rsbuild/core:
Notes:
next16.3+: not yet supported (next-rspackmoved to@rspack/core2.x, whilestorybook-next-rsbuildis still on 1.x). Pinnextandnext-rspackto16.2.x; the startup check reports this case with the same guidance.next-rspackmust be installed at the exact same version asnext.- Only the
@rsbuild/coreversions listed above resolve to the matching@rspack/core— a newer@rsbuild/core, even within the same minor, usually shifts its@rspack/corepin and fails the startup check. - If startup aborts with an
@rspack/coremismatch error, compare the two versions and paths it prints (the check is strict — any difference aborts startup):- Different versions — realign using this table, or force
@rspack/coreto the target version with pnpmoverrides/ yarnresolutions. - Same version, different paths (the error reads "duplicate physical copies") — your package manager installed two copies of the same version (yarn Berry is the known offender, splitting on
@rspack/core's optional@swc/helperspeer). Pin the splitting peer (e.g. add@swc/helperstoresolutions/overrides) or runyarn dedupe/pnpm dedupe— changing the@rspack/coreversion won't help, because it already matches.
- Different versions — realign using this table, or force
Getting started
Installation
Install storybook-next-rsbuild together with next-rspack pinned to your exact next version — a bare next-rspack install pulls the registry's latest, which the startup check rejects when it doesn't match your next — plus the @rsbuild/core from your version matrix row. For example, on next@16.2.3:
If the framework can't load your Next.js config (for example, next-rspack isn't installed), what happens depends on the build mode:
storybook devstill boots, with React support only, and logs what went wrong — CSS, fonts, images, and the navigation mocks won't work until the problem is fixed.storybook buildfails with the original error, so CI catches the problem instead of publishing a Storybook where every Next.js feature is silently missing.
Set the allowMissingNextBridge option to true if you intentionally want a production build with React support only.
Configure .storybook/main.ts
That's it — the framework auto-detects next.config.{js,ts,mjs} at the project root. If your config lives elsewhere, set nextConfigPath:
A relative nextConfigPath resolves against the Storybook config directory (.storybook); an absolute path is used as-is.
Configuration mental model
Under the hood, the framework loads your next.config.{js,ts} with Next.js's own config loader and applies the resulting build settings — compiler options, aliases, environment variables, custom webpack additions — to the Storybook build. The practical consequence is the one thing to internalize:
Whatever Next.js compiles → configure it in
next.config.ts(Next.js style). Whatever belongs to the Storybook preview build itself → configure it in.storybook/main.ts(Rsbuild style viarsbuildFinal, or webpack style viawebpackFinal).
There is no third, framework-specific config surface to learn or keep in sync. The settings that make next dev work already make Storybook work, because the same next.config.ts drives both — in both dev (storybook dev) and production (storybook build), each loading your config in the matching mode, just like next dev vs next build.
Who owns what
The split gives each tool what it does best: Next.js compiles your components; Rsbuild handles CSS; Storybook renders the preview and owns the React runtime.
Deciding where a setting goes
- Would you put it in
next.config.tsto makenext dev/next buildwork? Leave it there — Storybook picks it up automatically. Don't duplicate it into.storybook. One exception: theturbopackkey is not read (Storybook consumes Next.js's webpack-side configuration), so Turbopack loader rules (SVGR etc.) must be mirrored via thewebpack()snippet. - Is it a CSS preprocessor that needs a plugin (Sass, Less, Stylus)? Add the matching Rsbuild plugin via
rsbuildFinal. Rsbuild — not Next.js — runs the CSS pipeline in Storybook, so you extend it the Rsbuild way. (CSS Modules, plain CSS, and PostCSS/Tailwind need nothing.) - Is it a tweak only the Storybook preview needs (an alias just for stories, a rule mutation)? Use
rsbuildFinalfor an alias or an Rsbuild plugin; usewebpackFinalonly to mutate an existing rspack rule — seersbuildFinalvswebpackFinal.
rsbuildFinal vs webpackFinal
Both hooks extend the Storybook preview build, but they are not interchangeable — pick by altitude:
rsbuildFinalis the preferred, high-level surface. Reach for it first: add Rsbuild plugins (pluginSass()),source.defineentries, or a stories-onlyresolve.alias. It's the surface the framework's own types advertise.webpackFinalis the low-level escape hatch. Use it only to introspect or mutate an existing rspack rule — e.g. taking.svgaway from Rsbuild's default asset rule for SVGR. Under this framework,webpackFinalruns late, against the fully-assembled config, so the rules you want to read already exist.
Under this framework, the webpackFinal of any addon registered in addons already runs against the fully-assembled config, so you do not need to also list that addon under webpackAddons (the mechanism other Rsbuild frameworks use). If you list the same addon in both addons and webpackAddons, the framework runs its webpackFinal exactly once and logs which duplicate it skipped — remove the webpackAddons entry to clear the warning.
Same as @storybook/nextjs-vite, vs. different here
The runtime behavior — decorators, mocks, and story-authoring APIs — is ported from @storybook/nextjs-vite. So for everything in the left column, follow the official Storybook docs; this page only links to them. The right column is where this framework genuinely differs — that content is written out below.
Rule of thumb: if a Next.js or Storybook feature isn't covered on this page at all, default to the official
@storybook/nextjs-vitedocs — the runtime is ported from it. The one thing that never carries over is build/bundler configuration: ignore upstream'sviteFinalguidance and drive the build throughnext.config.tsplusrsbuildFinal/webpackFinal, as described above.
Framework options
TypeScript settings (including typescript.reactDocgen) type-check on main.ts and behave as documented in the Configuration guide.
Custom webpack settings
If your project customizes webpack in next.config.ts, your rules, aliases, fallbacks, externals, and experiments are applied to the Storybook build automatically.
The framework captures what your webpack() hook adds; what carries over differs by field:
One consequence worth calling out: editing a Next.js built-in rule in place doesn't reach Storybook. The canonical SVGR recipe (fileLoaderRule.exclude = /\.svg$/) is exactly that — an in-place edit. To take .svg away from Rsbuild's asset rule, do it on the Storybook side with webpackFinal — see SVGR.
Plugins are the exception. They're gated behind the forwardNextConfigPlugins option (default false), because most plugins people add target Next.js's production pipeline (build-manifest writers, source-map uploaders, stats emitters) and either do nothing in Storybook or crash the build (copy-webpack-plugin is a known case). When the gate is closed, dropped plugins are logged by name. Opt in only for a client-side plugin you've verified works with rspack:
Two narrow exceptions beyond plugins are filtered on purpose:
- Framework-reserved aliases —
react/react-dom/react-server-dom-webpack, plusnext/imageandstyled-jsx— set innext.config.webpack()are dropped (with a warning), whether spelled plainly or with a trailing$. Storybook must own the React copy (a second React breaks hooks and context), and the framework must own thenext/imagemock and the styled-jsx identity, so there is intentionally no escape hatch for repointing them. .mdxrules (e.g. from@next/mdx) are dropped (with an info log): in Storybook,.mdxis owned by@storybook/addon-docs. Your page-MDX loader still applies to real Next.js pages, just not to Storybook docs.
Supported Next.js features
next/image
next/image renders inside stories. Storybook has no image-optimization server, so the framework serves images directly — no /_next/image endpoint and no sharp install needed; local and remote src values both render.
The usage, local-vs-remote behavior, and the "image imports return an object" rule are identical to upstream — see Next.js's Image component. A few notes specific to this framework:
- Local
srclike/vercel.svgresolves from Next.js'spublic/dir only if you add it tostaticDirs(see the main.ts example). - Per-story
next/imageconfig is applied viaparameters.nextjs.image, only when set. - Static image imports resolve to
StaticImageData.import img from './x.png'yields the{ src, width, height, blurDataURL }object — matching upstream andnext build— so<Image src={img} />gets intrinsic dimensions andplaceholder="blur"works. (.svgimports are left to your SVGR setup and are not rewritten toStaticImageData.) next/legacy/imageworks too. It's served the same way asnext/image, so legacy stories render without a/_next/imageendpoint.
next/font
Both next/font/google and next/font/local work out of the box, with no staticDirs mapping required — the framework resolves your fonts at build time and injects the @font-face/class CSS when the story renders.
The supported surface — including the not-supported options (fallback, adjustFontFallback, preload/display ignored) and the NEXT_FONT_GOOGLE_MOCKED_RESPONSES CI mocking advice — matches upstream. See Next.js font optimization.
next/head
Supported out of the box through a built-in decorator that updates document.head. Children land in the preview iframe's <head>, exactly as documented upstream — see Next.js Head.
next/link & next/dynamic
Both ship unchanged. next/link navigates through the mocked router (see Routing); next/dynamic lazy chunks resolve with no special wiring. Routing behavior is documented upstream — see Next.js routing.
Routing & navigation
Both routers are always active. Unlike
@storybook/nextjs-vite, this framework mounts the App Router (next/navigation) and the Pages Router (next/router) contexts on every story — there is no router selector. Aparameters.nextjs.appDirectoryflag has no effect here (and is not part of the exported types), so a single story can readnext/navigationandnext/routerindependently. This is deliberate: it's what mixed-router (Next.js 13+) projects need.
If you're migrating from @storybook/nextjs-vite, remove appDirectory from your parameters — it's accepted-but-ignored, and the App/Pages hooks work regardless.
App Router — next/navigation
Seed the navigation context that usePathname, useSearchParams, useParams, and the layout-segment hooks read through parameters.nextjs.navigation (pathname, query, and segments). The hook behavior and the default context ({ pathname: '/', query: {} }) are inherited from upstream — see Next.js navigation.
Route params & layout segments
useSelectedLayoutSegment, useSelectedLayoutSegments, and useParams are driven by parameters.nextjs.navigation.segments, which accepts two forms:
string[]— a parallel-route path that builds the layout-segment tree, e.g.segments: ['dashboard', 'analytics'].[key, value][]tuples (or a plain object) — explicit route params returned byuseParams(), e.g.segments: [['address', '0xdeadbeef']]→useParams()returns{ address: '0xdeadbeef' }.
The hook return semantics match upstream — see the useSelectedLayoutSegment(s) / useParams hooks.
Pages Router — next/router
Seed Pages Router stories with parameters.nextjs.router. The accepted shape and the default router state (pathname: '/', isReady: true, …) are inherited from upstream — see Next.js routing and the default router.
Parameters reference
Styling
CSS, CSS Modules, PostCSS / Tailwind, styled-jsx
Rsbuild owns the CSS pipeline (the deliberate division of labor from the mental model). CSS Modules, global CSS imports, PostCSS, and Tailwind work out of the box with no extra wiring and behave the same as in your Next.js app. styled-jsx works too — it's compiled by Next.js's compiler like the rest of your components.
Usage matches upstream — see CSS Modules, Tailwind / PostCSS, and Styled JSX. The only thing to know is who owns it: because Rsbuild runs the pipeline (not Next.js), custom PostCSS/Tailwind config is picked up by Rsbuild's auto-detection, and preprocessors are opt-in — see next.
Write postcss.config plugins in object form, not the array-of-strings shorthand
Because Rsbuild loads your postcss.config.{js,mjs,ts} (via postcss-load-config) instead of Next.js, the bare array-of-strings plugin shorthand is not resolved:
That shorthand is a Next.js-specific extension (Next requires the strings itself); postcss-load-config — used by Rsbuild, plain webpack's postcss-loader, Vite, etc. — only resolves plugin name strings in the object form. Use that instead (it's valid in Next.js too, so the same file keeps working for next dev/next build):
An array of already-instantiated plugins (plugins: [tailwindcss(), cssnano()]) works as well.
Sass / Less
This is the one styling behavior that differs from official Next.js Storybook. Upstream inherits Next.js's built-in Sass support with zero config; here, because Rsbuild owns the CSS pipeline, Sass and Less are opt-in through an Rsbuild plugin, and Sass options in next.config are not applied. If you import a .scss/.sass file without a Sass loader configured, the framework emits a one-time warning pointing back here.
Install the plugin and merge it via rsbuildFinal:
Pick the @rsbuild/plugin-sass version compatible with the @rsbuild/core your version matrix row pins. Less works the same way with @rsbuild/plugin-less.
CSS-in-JS (styled-components via SWC, Emotion at runtime)
These are Next.js compiler transforms, so you enable them the Next.js way — in next.config.ts — and stories are compiled with them the same way your app is. No Storybook-side wiring:
Emotion needs no special transform — it's runtime CSS-in-JS and works as-is. (Next.js's compiler.emotion transform is optional, and honored too if you enable it.)
Compilation & module resolution
SWC transforms, transpilePackages, optimizePackageImports
Stories are compiled with Next.js's own compiler (SWC), so build behavior matches your app:
'use client'directives behave like they do in Next.js, andserver-onlymodules resolve as in a real build.transpilePackagesentries in yournext.config.tsapply to stories automatically.optimizePackageImports(default-on in Next 15+) is honored, including packages whose published source is TypeScript.- JSX runtime selection follows your
next.config.ts.
These are Next.js-owned concerns: configure them in next.config.ts and they apply to stories automatically. (TypeScript behavior matches upstream — see Typescript.)
Imports, aliases & tsconfig paths
Root-relative absolute imports, module aliases (@/...), Node-standard subpath imports (#... from package.json#imports), and tsconfig.json baseUrl/paths all resolve, because the framework applies Next.js's resolved aliases. The behavior — and the "absolute imports cannot be mocked" caveat — matches upstream. See Imports.
Environment variables
NEXT_PUBLIC_* variables and next.config.ts's env key reach your stories automatically — they're inlined at build time, exactly as next dev / next build do. .env* files are picked up too, in the matching build mode: storybook dev loads .env.development[.local], and storybook build loads .env.production[.local] (both also load the base .env / .env.local). There's nothing to redefine via rsbuildFinal's source.define.
One limit mirrors a real build: server-only env vars — those without the NEXT_PUBLIC_ prefix — are not inlined into the client bundle, so they read as undefined in stories, exactly as they would in a client component under next build.
node: protocol & Node builtins
Importing Node builtins in browser-bound code won't crash the Storybook build. Bare builtins (fs, path, querystring, …) and node:-prefixed imports (node:path, even node:sqlite) resolve to browser-safe stand-ins — an empty module, or a polyfill where Next.js supplies one. The Buffer / process globals that some libraries expect (e.g. next-auth, openid-client) are also provided. There's nothing to configure.
Custom loaders (SVGR)
A few setups need a change in both config files, because Next.js's build config and Storybook's preview config each cover part of the job. SVGR is the canonical case: you add the loader rule in next.config.ts (so both next dev and Storybook get it), and you also take .svg away from Rsbuild's default asset rule — a Storybook-side concern — via webpackFinal. Your webpackFinal runs against the fully assembled config, so it can inspect and mutate existing rules. (If webpackFinal adds a rule that matches the same files as a rule from next.config.ts, the framework keeps only the Storybook-side one and logs it, so files aren't processed twice. Rules scoped to different files — via include/exclude/resourceQuery/issuer — are both kept.)
Mocking Next.js APIs in stories
For interaction testing and manual overrides, the framework ships subpath exports that mirror @storybook/nextjs-vite. Each entry re-exports the real Next.js module and wraps select APIs with spy-able fn() mocks from storybook/test. Only the package name differs — the API surface and behavior are ported verbatim, so the upstream reference applies (linked per row below).
Call getRouter() from inside a play function to assert router interactions. Use navigation.mock for App Router stories and router.mock for Pages Router stories:
To mock your own (non-Next.js) modules, use Storybook's module mocking guide.
Caveats:
- Singleton state.
getRouter()returns the instance seeded by the most recent story render. Don't hold the reference across stories — read it fresh inside eachplay. - Client-side only. Storybook does not run a Next.js server, so
cache.mockandheaders.mockare the only way server-only APIs resolve when imported from client components in stories. - Coupled to Next.js internals. A
nextupgrade can move the modules these entries wrap — upgradestorybook-next-rsbuildtogether withnext.
Runtime config
getConfig() and publicRuntimeConfig work in principle — because Storybook doesn't server-render, components see publicRuntimeConfig (not serverRuntimeConfig), the same as upstream (see Runtime config).
One delta: the legacy next/config import is not available. Next.js 16 removed next/config from its package exports, so getConfig() imported from next/config no longer resolves on the supported Next 16 line.
Known limitations
- No Server Components runtime. Components marked
'use client'render; pure Server Components are not executed. Note this framework does not expose theexperimentalRSCSuspense-wrapper path that@storybook/nextjs-vitedocuments — only client components render. - No API routes, middleware, or server actions. Storybook doesn't run a Next.js server —
route.ts,middleware.ts, and'use server'entry points don't execute. (Same as@storybook/nextjs-vite.) - No
/_next/imageoptimization.next/image(andnext/legacy/image) serve images directly; runtime behavior differs from production, where the image is optimized on the fly. turbopack.*config keys are ignored. Storybook consumes Next.js's webpack-side configuration, soturbopack.rules/resolveAlias/resolveExtensions(and the legacyexperimental.turbo) have no effect — the framework logs a warning when it finds them. Mirror Turbopack loader rules via thewebpack()snippet.- Sass/Less need an Rsbuild plugin. Preprocessor support is opt-in via
rsbuildFinalrather than inherited from Next.js (see Sass / Less). - Runtime config is dropped on Next 16+ (see Runtime config).
- Deployment/output keys mostly don't affect serving.
output(export/standalone),assetPrefix,trailingSlash, andrewrites/redirects/headershave no effect in Storybook — the preview is served at the root, so story assets (staticDirs,next/imagesrc) need nobasePathprefix.basePathis the exception: its value is still compiled into the client code, sonext/linkand router hrefs arebasePath-prefixed at runtime — same as@storybook/nextjs-vite. - Version coupling. The framework relies on Next.js internals. Any
nextpatch or minor release can break compatibility — upgradestorybook-next-rsbuildalongsidenext.
Next steps
- Writing Stories — the basics of Component Story Format (CSF).
- Interaction Testing — asserting against the router mocks above.
- Configuration —
rsbuildFinal, builder options, and TypeScript settings.
A complete, runnable reference lives in the repository at sandboxes/nextjs, covering App Router, Pages Router, next/font, next/image, CSS Modules, Tailwind, Sass, styled-components, Emotion, optimizePackageImports, transpilePackages, SVGR, and custom next.config.webpack() settings.