close

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.

Experimental

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

PackageVersion
next>=16.0.0 <16.3.0
next-rspackmatches next (same version)
react / react-dom^18.0.0 || ^19.0.0
storybook^10.5.0
@rsbuild/coresee version matrix

@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:

nextnext-rspack@rspack/core (transitive)@rsbuild/core
16.0.xsame as next1.6.01.6.0 or 1.6.1
16.1.xsame as next1.6.71.6.14
16.2.xsame as next1.6.71.6.14

Notes:

  • next 16.3+: not yet supported (next-rspack moved to @rspack/core 2.x, while storybook-next-rsbuild is still on 1.x). Pin next and next-rspack to 16.2.x; the startup check reports this case with the same guidance.
  • next-rspack must be installed at the exact same version as next.
  • Only the @rsbuild/core versions listed above resolve to the matching @rspack/core — a newer @rsbuild/core, even within the same minor, usually shifts its @rspack/core pin and fails the startup check.
  • If startup aborts with an @rspack/core mismatch 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/core to the target version with pnpm overrides / yarn resolutions.
    • 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/helpers peer). Pin the splitting peer (e.g. add @swc/helpers to resolutions/overrides) or run yarn dedupe / pnpm dedupe — changing the @rspack/core version won't help, because it already matches.

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:

npm
yarn
pnpm
bun
deno
npm install storybook-next-rsbuild next-rspack@16.2.3 @rsbuild/core@1.6.14 -D

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 dev still 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 build fails 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

.storybook/main.ts
import type { 
import StorybookConfig
StorybookConfig
} from 'storybook-next-rsbuild'
const
const config: StorybookConfig
config
:
import StorybookConfig
StorybookConfig
= {
framework: string
framework
: 'storybook-next-rsbuild',
stories: string[]
stories
: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: string[]
addons
: ['@storybook/addon-docs'],
// Needed if stories reference assets from Next.js's `public/` dir // (e.g. <Image src="/vercel.svg" />).
staticDirs: string[]
staticDirs
: ['../public'],
} export default
const config: StorybookConfig
config

That's it — the framework auto-detects next.config.{js,ts,mjs} at the project root. If your config lives elsewhere, set nextConfigPath:

.storybook/main.ts
import type { 
import StorybookConfig
StorybookConfig
} from 'storybook-next-rsbuild'
const
const config: StorybookConfig
config
:
import StorybookConfig
StorybookConfig
= {
framework: {
    name: string;
    options: {
        nextConfigPath: string;
    };
}
framework
: {
name: string
name
: 'storybook-next-rsbuild',
options: {
    nextConfigPath: string;
}
options
: {
nextConfigPath: string
nextConfigPath
: '../next.config.ts',
}, },
stories: string[]
stories
: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
} export default
const config: StorybookConfig
config

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 via rsbuildFinal, or webpack style via webpackFinal).

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

ConcernOwnerConfigure inStyle
'use client' / server-only, JSX runtime, SWC transformsNext.jsnext.config.ts (automatic)Next.js
transpilePackages, optimizePackageImportsNext.jsnext.config.tsNext.js
Env vars (NEXT_PUBLIC_*, next.config.env, .env* files)Next.jsnext.config.ts / .env* (automatic)Next.js
compiler.* (e.g. styledComponents, emotion)Next.jsnext.config.tsNext.js
Module resolve.alias / fallback, custom loaders (e.g. SVGR) — except react* aliases (pinned by Storybook)Next.jsnext.config.tswebpack()Next.js
next/font, next/imageFrameworknext.config.ts (automatic)Next.js
CSS Modules, global CSS, PostCSS / TailwindRsbuildworks out of the box
Sass / Less preprocessorsRsbuild.storybook/main.tsrsbuildFinalRsbuild
Storybook-preview-only build tweaksStorybook builder.storybook/main.tsrsbuildFinal / webpackFinalRsbuild / webpack

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

  1. Would you put it in next.config.ts to make next dev / next build work? Leave it there — Storybook picks it up automatically. Don't duplicate it into .storybook. One exception: the turbopack key is not read (Storybook consumes Next.js's webpack-side configuration), so Turbopack loader rules (SVGR etc.) must be mirrored via the webpack() snippet.
  2. 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.)
  3. Is it a tweak only the Storybook preview needs (an alias just for stories, a rule mutation)? Use rsbuildFinal for an alias or an Rsbuild plugin; use webpackFinal only to mutate an existing rspack rule — see rsbuildFinal vs webpackFinal.

rsbuildFinal vs webpackFinal

Both hooks extend the Storybook preview build, but they are not interchangeable — pick by altitude:

  • rsbuildFinal is the preferred, high-level surface. Reach for it first: add Rsbuild plugins (pluginSass()), source.define entries, or a stories-only resolve.alias. It's the surface the framework's own types advertise.
  • webpackFinal is the low-level escape hatch. Use it only to introspect or mutate an existing rspack rule — e.g. taking .svg away from Rsbuild's default asset rule for SVGR. Under this framework, webpackFinal runs late, against the fully-assembled config, so the rules you want to read already exist.
Addon webpackFinal runs once here — webpackAddons is unnecessary

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-vite docs — the runtime is ported from it. The one thing that never carries over is build/bundler configuration: ignore upstream's viteFinal guidance and drive the build through next.config.ts plus rsbuildFinal / webpackFinal, as described above.

Framework options

OptionTypeDefaultDescription
nextConfigPathstringauto-detectedPath to a next.config.{js,ts,mjs} file. Absolute, or relative to .storybook.
forwardNextConfigPluginsbooleanfalseForward plugins added by next.config.webpack() into Storybook's build. Rules, aliases, fallbacks, and externals are always forwarded — only plugins are gated. See below.
allowMissingNextBridgebooleanfalseLet a production storybook build fall back to React-only support (instead of failing) when the Next.js config can't be loaded. storybook dev always falls back; only the production build is gated. Enable for an intentional React-only static build.
imageobject{}Accepted for compatibility with @storybook/nextjs; has no effect here. Per-story image config is set via parameters.nextjs.image, not this option.
builderBuilderOptions{}Options forwarded to storybook-builder-rsbuild. See the Configuration guide.

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:

FieldWhat carries over
module.rulesNew rules you add. Options set on the next-swc-loader entry carry over too.
resolve.alias / resolve.fallback / experimentsAdded keys and changed values.
externalsNew entries you add.
Everything else (optimization, cache, output, devtool, …)Not carried over — Storybook keeps its own settings. Deletions and wholesale replacements (e.g. config.module.rules = []) are ignored too.

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:

.storybook/main.ts
import type { 
import StorybookConfig
StorybookConfig
} from 'storybook-next-rsbuild'
const
const config: StorybookConfig
config
:
import StorybookConfig
StorybookConfig
= {
framework: {
    name: string;
    options: {
        forwardNextConfigPlugins: boolean;
    };
}
framework
: {
name: string
name
: 'storybook-next-rsbuild',
options: {
    forwardNextConfigPlugins: boolean;
}
options
: {
// Forward client-side plugin instances (DefinePlugin values always carry over).
forwardNextConfigPlugins: boolean
forwardNextConfigPlugins
: true,
}, },
stories: string[]
stories
: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
} export default
const config: StorybookConfig
config

Two narrow exceptions beyond plugins are filtered on purpose:

  • Framework-reserved aliasesreact / react-dom / react-server-dom-webpack, plus next/image and styled-jsx — set in next.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 the next/image mock and the styled-jsx identity, so there is intentionally no escape hatch for repointing them.
  • .mdx rules (e.g. from @next/mdx) are dropped (with an info log): in Storybook, .mdx is 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 src like /vercel.svg resolves from Next.js's public/ dir only if you add it to staticDirs (see the main.ts example).
  • Per-story next/image config is applied via parameters.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 and next build — so <Image src={img} /> gets intrinsic dimensions and placeholder="blur" works. (.svg imports are left to your SVGR setup and are not rewritten to StaticImageData.)
  • next/legacy/image works too. It's served the same way as next/image, so legacy stories render without a /_next/image endpoint.
Image.stories.tsx
import 
import Image
Image
from 'next/image'
import type {
import Meta
Meta
,
import StoryObj
StoryObj
} from 'storybook-next-rsbuild'
const
const meta: Meta<any>
meta
= {
component: any
component
:
import Image
Image
,
args: {
    src: string;
    alt: string;
    width: number;
    height: number;
}
args
: {
src: string
src
: '/vercel.svg',
alt: string
alt
: 'Vercel',
width: number
width
: 200,
height: number
height
: 48 },
} satisfies
import Meta
Meta
<typeof
import Image
Image
>
export default
const meta: Meta<any>
meta
export const
const Default: StoryObj<Meta<any>>
Default
:
import StoryObj
StoryObj
<typeof
const meta: Meta<any>
meta
> = {}

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.

Font.stories.tsx
import { 
import Inter
Inter
} from 'next/font/google'
import type {
import Meta
Meta
,
import StoryObj
StoryObj
} from 'storybook-next-rsbuild'
const
const inter: any
inter
=
import Inter
Inter
({
subsets: string[]
subsets
: ['latin'] })
function
function FontDemo({ className }: {
    className: string;
}): JSX.Element
FontDemo
({
className: string
className
}: {
className: string
className
: string }) {
return <
JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
HTMLAttributes<HTMLParagraphElement>.className?: string | undefined
className
={
className: string
className
}>The quick brown fox jumps over the lazy dog.</
JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>
} const
const meta: Meta<({ className }: {
    className: string;
}) => JSX.Element>
meta
= {
component: ({ className }: {
    className: string;
}) => JSX.Element
component
:
function FontDemo({ className }: {
    className: string;
}): JSX.Element
FontDemo
} satisfies
import Meta
Meta
<typeof
function FontDemo({ className }: {
    className: string;
}): JSX.Element
FontDemo
>
export default
const meta: Meta<({ className }: {
    className: string;
}) => JSX.Element>
meta
export const
const Default: StoryObj<Meta<({ className }: {
    className: string;
}) => JSX.Element>>
Default
:
import StoryObj
StoryObj
<typeof
const meta: Meta<({ className }: {
    className: string;
}) => JSX.Element>
meta
> = {
args: {
    className: any;
}
args
: {
className: any
className
:
const inter: any
inter
.className },
}

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.

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. A parameters.nextjs.appDirectory flag has no effect here (and is not part of the exported types), so a single story can read next/navigation and next/router independently. 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.

Navigation.stories.tsx
import { 
import useRouter
useRouter
,
import usePathname
usePathname
,
import useSearchParams
useSearchParams
} from 'next/navigation'
import type {
import Meta
Meta
,
import StoryObj
StoryObj
} from 'storybook-next-rsbuild'
function
function Component(): JSX.Element
Component
() {
const
const router: any
router
=
import useRouter
useRouter
()
const
const pathname: any
pathname
=
import usePathname
usePathname
()
const
const searchParams: any
searchParams
=
import useSearchParams
useSearchParams
()
return ( <
JSX.IntrinsicElements.button: DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
ButtonHTMLAttributes<HTMLButtonElement>.type?: "button" | "submit" | "reset" | undefined
type
="button"
DOMAttributes<HTMLButtonElement>.onClick?: MouseEventHandler<HTMLButtonElement> | undefined
onClick
={() =>
const router: any
router
.push('/next')}>
Navigate from {
const pathname: any
pathname
}?{
const searchParams: any
searchParams
.toString()}
</
JSX.IntrinsicElements.button: DetailedHTMLProps<ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
>
) } export default {
component: () => JSX.Element
component
:
function Component(): JSX.Element
Component
,
parameters: {
    nextjs: {
        navigation: {
            pathname: string;
            query: {
                foo: string;
            };
        };
    };
}
parameters
: {
nextjs: {
    navigation: {
        pathname: string;
        query: {
            foo: string;
        };
    };
}
nextjs
: {
// No `appDirectory` needed — both routers are always mounted.
navigation: {
    pathname: string;
    query: {
        foo: string;
    };
}
navigation
: {
pathname: string
pathname
: '/hello',
query: {
    foo: string;
}
query
: {
foo: string
foo
: 'bar' },
}, }, }, } satisfies
import Meta
Meta
<typeof
function Component(): JSX.Element
Component
>
export const
const Default: StoryObj<() => JSX.Element>
Default
:
import StoryObj
StoryObj
<typeof
function Component(): JSX.Element
Component
> = {}

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 by useParams(), e.g. segments: [['address', '0xdeadbeef']]useParams() returns { address: '0xdeadbeef' }.

The hook return semantics match upstream — see the useSelectedLayoutSegment(s) / useParams hooks.

RouteParams.stories.tsx
import { 
import useParams
useParams
} from 'next/navigation'
import type {
import Meta
Meta
,
import StoryObj
StoryObj
} from 'storybook-next-rsbuild'
function
function Profile(): JSX.Element
Profile
() {
const {
const address: any
address
} =
import useParams
useParams
()
return <
JSX.IntrinsicElements.span: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span
>Address: {
const address: any
address
}</
JSX.IntrinsicElements.span: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span
>
} export default {
component: () => JSX.Element
component
:
function Profile(): JSX.Element
Profile
,
parameters: {
    nextjs: {
        navigation: {
            segments: string[][];
        };
    };
}
parameters
: {
nextjs: {
    navigation: {
        segments: string[][];
    };
}
nextjs
: {
navigation: {
    segments: string[][];
}
navigation
: {
segments: string[][]
segments
: [['address', '0xdeadbeef']] },
}, }, } satisfies
import Meta
Meta
<typeof
function Profile(): JSX.Element
Profile
>
export const
const Default: StoryObj<() => JSX.Element>
Default
:
import StoryObj
StoryObj
<typeof
function Profile(): JSX.Element
Profile
> = {}

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.

PagesRouter.stories.tsx
import { 
import useRouter
useRouter
} from 'next/router'
import type {
import Meta
Meta
,
import StoryObj
StoryObj
} from 'storybook-next-rsbuild'
function
function Component(): JSX.Element
Component
() {
const
const router: any
router
=
import useRouter
useRouter
()
return <
JSX.IntrinsicElements.span: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span
>Path: {
const router: any
router
.pathname}</
JSX.IntrinsicElements.span: DetailedHTMLProps<HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>
span
>
} export default {
component: () => JSX.Element
component
:
function Component(): JSX.Element
Component
,
parameters: {
    nextjs: {
        router: {
            pathname: string;
            query: {
                id: string;
            };
        };
    };
}
parameters
: {
nextjs: {
    router: {
        pathname: string;
        query: {
            id: string;
        };
    };
}
nextjs
: {
router: {
    pathname: string;
    query: {
        id: string;
    };
}
router
: {
pathname: string
pathname
: '/pages-route',
query: {
    id: string;
}
query
: {
id: string
id
: '42' } },
}, }, } satisfies
import Meta
Meta
<typeof
function Component(): JSX.Element
Component
>
export const
const Default: StoryObj<() => JSX.Element>
Default
:
import StoryObj
StoryObj
<typeof
function Component(): JSX.Element
Component
> = {}

Parameters reference

ParameterApplies toShapeNotes
nextjs.navigationApp Router (next/navigation){ pathname?, query?, segments? }The exported type is Partial<NextRouter>, but at runtime the object also accepts segments (the route-param/segment driver).
nextjs.routerPages Router (next/router)Partial<NextRouter>Seeds the stubbed Pages Router.
nextjs.imagenext/imagePartial<ImageProps>Per-story default ImageProps applied to next/image (e.g. priority, quality, loader).
nextjs.appDirectoryAccepted but ignored. Both routers always mount; kept only for nextjs-vite source compatibility.

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:

// ❌ Rejected here — `Invalid PostCSS Plugin found at: plugins[0]`
export default { plugins: ['@tailwindcss/postcss', 'cssnano'] }

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):

// ✅ Works in both Next.js and Storybook
export default {
  plugins: {
    '@tailwindcss/postcss': {},
    ...(process.env.NODE_ENV === 'production' ? { cssnano: {} } : {}),
  },
}

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:

npm
yarn
pnpm
bun
deno
npm install @rsbuild/plugin-sass -D
.storybook/main.ts
import { 
import mergeRsbuildConfig
mergeRsbuildConfig
} from '@rsbuild/core'
import {
import pluginSass
pluginSass
} from '@rsbuild/plugin-sass'
import type {
import StorybookConfig
StorybookConfig
} from 'storybook-next-rsbuild'
const
const config: StorybookConfig
config
:
import StorybookConfig
StorybookConfig
= {
framework: string
framework
: 'storybook-next-rsbuild',
stories: string[]
stories
: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
// Rsbuild owns the CSS pipeline — extend it the Rsbuild way.
rsbuildFinal: (config: any) => any
rsbuildFinal
: (
config: any
config
) =>
import mergeRsbuildConfig
mergeRsbuildConfig
(
config: any
config
, {
plugins: any[]
plugins
: [
import pluginSass
pluginSass
()] }),
} export default
const config: StorybookConfig
config

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:

next.config.ts
import type { 
import NextConfig
NextConfig
} from 'next'
const
const nextConfig: NextConfig
nextConfig
:
import NextConfig
NextConfig
= {
compiler: {
    styledComponents: boolean;
}
compiler
: {
styledComponents: boolean
styledComponents
: true,
}, } export default
const nextConfig: NextConfig
nextConfig

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, and server-only modules resolve as in a real build.
  • transpilePackages entries in your next.config.ts apply 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.)

next.config.ts
import type { 
import NextConfig
NextConfig
} from 'next'
const
const nextConfig: NextConfig
nextConfig
:
import NextConfig
NextConfig
= {
webpack: (config: any) => any
webpack
: (
config: any
config
) => {
config: any
config
.module?.rules?.push({
test: RegExp
test
: /\.svg$/,
use: string[]
use
: ['@svgr/webpack'] })
return
config: any
config
}, } export default
const nextConfig: NextConfig
nextConfig
.storybook/main.ts
import type { 
import StorybookConfig
StorybookConfig
} from 'storybook-next-rsbuild'
const
const config: StorybookConfig
config
:
import StorybookConfig
StorybookConfig
= {
framework: string
framework
: 'storybook-next-rsbuild',
stories: string[]
stories
: ['../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
// Exclude .svg from Rsbuild's default asset rule so the SVGR rule added in // next.config.webpack() is the one that processes them.
webpackFinal: (config: any) => Promise<any>
webpackFinal
: async (
config: any
config
) => {
for (const
const rule: any
rule
of
config: any
config
.module?.rules ?? []) {
// Match ONLY Rsbuild's default asset rule — it carries a `oneOf`. The // @svgr/webpack rule from next.config also has `test: /\.svg$/i` but no // `oneOf`; excluding .svg from it too would disable SVGR and every // `*.svg` would be parsed as raw JS. if (
const rule: any
rule
&&
typeof
const rule: any
rule
=== 'object' &&
const rule: any
rule
.test instanceof
var RegExp: RegExpConstructor
RegExp
&&
const rule: any
rule
.test.test('probe.svg') &&
var Array: ArrayConstructor
Array
.
ArrayConstructor.isArray(arg: any): arg is any[]
isArray
(
const rule: any
rule
.oneOf)
) { const
const prev: any
prev
= (
const rule: any
rule
as any).exclude
;(
const rule: any
rule
as any).exclude =
var Array: ArrayConstructor
Array
.
ArrayConstructor.isArray(arg: any): arg is any[]
isArray
(
const prev: any
prev
)
? [...
const prev: any[]
prev
, /\.svg$/]
:
const prev: any
prev
? [
const prev: any
prev
, /\.svg$/]
: /\.svg$/ } } return
config: any
config
}, } export default
const config: StorybookConfig
config

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).

ImportUse forUpstream reference
storybook-next-rsbuild/navigation.mockApp Router — useRouter, usePathname, useSearchParams, redirect, notFound, useParams, … + getRouter()navigation.mock
storybook-next-rsbuild/router.mockPages Router — useRouter, withRouter, the singleton router + getRouter()router.mock
storybook-next-rsbuild/cache.mockrevalidatePath, revalidateTag, unstable_cache, unstable_noStorecache.mock
storybook-next-rsbuild/headers.mockheaders, cookies, draftMode (writable: headers().set(...), cookies().set(...))headers.mock

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:

Navigation.stories.tsx
import { 
import expect
expect
,
import userEvent
userEvent
,
import within
within
} from 'storybook/test'
import {
import getRouter
getRouter
} from 'storybook-next-rsbuild/navigation.mock'
import type {
import StoryObj
StoryObj
} from 'storybook-next-rsbuild'
export const
const Default: StoryObj
Default
:
import StoryObj
StoryObj
= {
play: ({ canvasElement }: {
    canvasElement: any;
}) => Promise<void>
play
: async ({
canvasElement: any
canvasElement
}) => {
const
const canvas: any
canvas
=
import within
within
(
canvasElement: any
canvasElement
)
const
const router: any
router
=
import getRouter
getRouter
()
await
import userEvent
userEvent
.click(
const canvas: any
canvas
.getByRole('button'))
await
import expect
expect
(
const router: any
router
.push).toHaveBeenCalledWith('/next')
}, }

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 each play.
  • Client-side only. Storybook does not run a Next.js server, so cache.mock and headers.mock are the only way server-only APIs resolve when imported from client components in stories.
  • Coupled to Next.js internals. A next upgrade can move the modules these entries wrap — upgrade storybook-next-rsbuild together with next.

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 the experimentalRSC Suspense-wrapper path that @storybook/nextjs-vite documents — 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/image optimization. next/image (and next/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, so turbopack.rules / resolveAlias / resolveExtensions (and the legacy experimental.turbo) have no effect — the framework logs a warning when it finds them. Mirror Turbopack loader rules via the webpack() snippet.
  • Sass/Less need an Rsbuild plugin. Preprocessor support is opt-in via rsbuildFinal rather 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, and rewrites/redirects/headers have no effect in Storybook — the preview is served at the root, so story assets (staticDirs, next/image src) need no basePath prefix. basePath is the exception: its value is still compiled into the client code, so next/link and router hrefs are basePath-prefixed at runtime — same as @storybook/nextjs-vite.
  • Version coupling. The framework relies on Next.js internals. Any next patch or minor release can break compatibility — upgrade storybook-next-rsbuild alongside next.

Next steps

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.