Join live: How to use parallel agents to fix a11y issues
Docs
Storybook Docs

Storybook for TanStack React

Storybook for TanStack React is Storybook's framework integration for TanStack Router and TanStack Start applications built with React and Vite.

It builds on @storybook/react-vite to add router-aware story rendering, automatic router mocking, and mocked TanStack Start server functions. Components that depend on routing or server functions can render inside Storybook without booting your full app runtime.

Install

To install Storybook in an existing TanStack Router or TanStack Start project, run this command in your project's root directory:

npm create storybook@latest

You can then get started writing stories, running tests, and documenting your components. For more control over the installation process, refer to the installation guide.

Requirements

  • React โ‰ฅ 18

  • Vite โ‰ฅ 7

This integration expects a TanStack Router application with @tanstack/react-router available in your project. If your app uses TanStack Start APIs such as server functions, keep the matching TanStack Start packages installed as well.

Run Storybook

To run Storybook for a particular project, run the following:

npm run storybook

To build Storybook, run:

npm run build-storybook

You will find the output in the configured outputDir (default is storybook-static).

Configure

Storybook for TanStack React uses Vite through @storybook/builder-vite and automatically wraps each story in a memory-backed TanStack Router. This gives you a working router context in Storybook without having to boot your full application shell.

Out of the box, it supports these workflows:

  • Supply a TanStack Route to render it as the story component
  • Set the initial route, params, and query string per story
  • Override loader, beforeLoad, and more on the story route without modifying the original route object via routeOverrides
  • Automatically mock @tanstack/react-router so navigation hooks work in stories and navigation attempts can be observed
  • Automatically stub TanStack Start server and runtime entry points so components using server functions can render in Storybook

Routing

Rendering a Route

Supply a TanStack Route object via parameters.tanstack.router.route. Storybook extracts the route's React component from the route and keeps the route available for typed router configuration.

Page.stories.ts
import type { Meta, StoryObj } from '@storybook/tanstack-react';
 
import { Route } from './Page';
 
const meta = {
  parameters: {
    layout: 'fullscreen',
    tanstack: {
      router: {
        route: Route, // ๐Ÿ‘ˆ Supply the Route here
        // ๐Ÿ‘‡ Rest of these properties are type-safe
        params: { id: '42' },
        query: { tab: 'details' },
      },
    },
  },
} satisfies Meta<typeof Route>;
 
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {};
 
export const WithCustomLoader: Story = {
  parameters: {
    tanstack: {
      router: {
        route: Route, // ๐Ÿ‘ˆ Supply the Route here
        // ๐Ÿ‘‡ Rest of these properties are type-safe
        params: { id: '42' },
        routeOverrides: {
          '/items/$id': {
            loader: async () => ({
              item: { id: '42', name: 'Loaded inside Storybook' },
            }),
          },
        },
      },
    },
  },
};
Handling dynamic params (e.g., /$id)

Supply params alongside routeOverrides under parameters.tanstack.router. The params object is interpolated into the URL, and routeOverrides lets you stub the loader without touching the original route.

Showcase.stories.ts
import type { Meta } from '@storybook/tanstack-react';
 
import { Route } from './$id';
 
const meta = {
  parameters: {
    tanstack: {
      router: {
        route: Route,
        params: { id: '42' },
        routeOverrides: {
          '/showcase/$id': {
            loader: () => ({ item: mockItem }),
          },
        },
      },
    },
  },
} satisfies Meta<typeof Route>;
 
export default meta;

For the full set of properties, see Parameters.

Rendering nested routes

When route is a file route connected to your app's route tree, Storybook automatically includes parent layout routes so the story renders inside the same nested hierarchy as the real app. You can also pass the routeTree export from routeTree.gen.ts directly.

Use path to navigate to the specific route, and routeOverrides to stub guards or loaders on ancestor routes so the story can render independently.

SettingsProfile.stories.ts
import type { Meta, StoryObj } from '@storybook/tanstack-react';
 
// ๐Ÿ‘‡ Route file is part of the app route tree
import { Route } from './routes/_authenticated/settings/profile';
 
const meta = {
  parameters: {
    tanstack: {
      router: {
        // ๐Ÿ‘‡ Storybook walks up the tree to root and duplicates the full route tree,
        //    so parent layouts (e.g. the authenticated shell) also render.
        route: Route,
        path: '/settings/profile',
        // ๐Ÿ‘‡ Stub out any parent route guards so the story can render standalone.
        routeOverrides: {
          '/_authenticated': { beforeLoad: () => {} },
        },
      },
    },
  },
} satisfies Meta<typeof Route>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {};

Using router parameters with a non-Route component

If your story renders a regular React component instead of a route object, you can still provide routing context through parameters.tanstack.router.

Page.stories.ts
import type { Meta, StoryObj } from '@storybook/tanstack-react';
 
import { Page } from './Page';
 
const meta = {
  component: Page,
} satisfies Meta<typeof Page>;
 
export default meta;
 
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {
  parameters: {
    tanstack: {
      router: {
        route: {
          path: '/demo/form/address',
        },
        query: { view: 'list' },
      },
    },
  },
};

This is useful when your component reads from hooks such as useRouterState, useSearch, useParams, or useLoaderData, but you do not want to make the route itself the story component.

Defining search params and URL fragments (hashes)

Use query for search params (e.g., ?tab=details&page=2) and path for a URL fragment (e.g., #section-name) under parameters.tanstack.router:

Page.stories.ts
import type { Meta, StoryObj } from '@storybook/tanstack-react';
 
import { Route } from './Page';
 
const meta = {
  parameters: {
    tanstack: {
      router: {
        route: Route,
      },
    },
  },
} satisfies Meta<typeof Route>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const WithHash: Story = {
  parameters: {
    tanstack: {
      // ๐Ÿ‘‡ Provide the URL fragment (hash) for the route
      router: { path: '/#section-name' },
    },
  },
};
 
export const WithSearch: Story = {
  parameters: {
    tanstack: {
      // ๐Ÿ‘‡ Provide the query string for the route
      router: { query: { tab: 'details', page: '2' } },
    },
  },
};

Overriding route options per story

When a route has a loader or beforeLoad that calls real APIs, you can override those options per story without modifying the original route object. Pass routeOverrides under parameters.tanstack.router. Each key is a route ID and the value can override loader, beforeLoad, validateSearch, loaderDeps, and context.

Use '__root__' as the key to target the root route.

UserCard.stories.ts
import type { Meta, StoryObj } from '@storybook/tanstack-react';
 
import { Route } from './UserCard';
 
const meta = {
  title: 'Users/UserCard',
  parameters: {
    tanstack: {
      router: {
        route: Route,
        params: { userId: '42' },
        // ๐Ÿ‘‡ Override the route's loader so the story doesn't call the real API.
        routeOverrides: {
          '/users/$userId': {
            loader: async () => ({ user: { id: '42', name: 'Ada Lovelace' } }),
          },
        },
      },
    },
  },
} satisfies Meta<typeof Route>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {};

Mocking

Automatic TanStack Router and TanStack Start mocks

This framework automatically redirects @tanstack/react-router imports to a Storybook-compatible mock layer. That mock re-exports TanStack Router APIs, keeps hooks such as useNavigate(), useSearch(), and useParams() available in stories, and wires navigation attempts into Storybook spies.

For TanStack Start apps, the integration also stubs TanStack Start server and runtime entry points. This is what allows components that depend on server functions or Start-specific runtime modules to render in Storybook without a running Start server.

In practice, this means you can usually render TanStack Start components directly, and createServerFn() handlers are replaced with mock functions that you can observe and override in stories and tests.

Mocking server functions in stories

If your component imports a TanStack Start server function, Storybook turns that createServerFn().handler(...) result into a mock function. That means you can override it per story with standard mock APIs.

For example, imagine your application code exports a server function like this:

src/lib/updateProfile.ts
import { createServerFn } from '@tanstack/start-client-core';
 
export const updateProfile = createServerFn({ method: 'POST' }).handler(
  async ({ data }: { data: { name: string } }) => {
    return { ok: true, name: data.name };
  },
);

In Storybook, you can override that function for each story:

ProfileForm.stories.ts
import type { Meta, StoryObj } from '@storybook/tanstack-react';
import { expect, mocked } from 'storybook/test';
 
import { updateProfile } from '../lib/updateProfile';
import { ProfileForm } from './ProfileForm';
 
const meta = {
  component: ProfileForm,
} satisfies Meta<typeof ProfileForm>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Success: Story = {
  beforeEach: async () => {
    mocked(updateProfile).mockResolvedValue({ ok: true, name: 'Ada Lovelace' });
  },
  play: async ({ canvas, userEvent }) => {
    await userEvent.type(canvas.getByLabelText('Name'), 'Ada Lovelace');
    await userEvent.click(canvas.getByRole('button', { name: 'Save profile' }));
 
    await expect(updateProfile).toHaveBeenCalled();
  },
};
 
export const Failure: Story = {
  beforeEach: async () => {
    mocked(updateProfile).mockRejectedValue(new Error('Could not save profile'));
  },
};

This is useful for documenting loading, success, and error states without changing your application code.

Handling server-only dependencies

TanStack Start apps often import server-only packages (e.g. database clients, auth libraries) at module scope inside route files. When Storybook loads the route tree, those imports can crash the browser. The integration handles this at three layers:

Framework-level mocks (automatic)

The preset already intercepts @tanstack/react-start, @tanstack/react-start/server, @tanstack/start-storage-context, and related TanStack modules. It also replaces createServerFn() handlers with mock functions. You do not need to do anything for these.

App-level server modules

When your routes import app-specific server code (e.g. ~/db/client, ~/auth/index.server), use Storybook's mocking with a __mocks__ file to prevent the real module (and its Node.js dependencies) from loading in the browser.

Step 1: Register the mock in .storybook/preview.ts:

.storybook/preview.ts
import { sb } from 'storybook/test';
 
// Prevents postgres (Node-only) from loading in the browser
sb.mock(import('../src/db/client.ts'));
 
export default {};

Step 2: Create src/db/__mocks__/client.ts next to the real module. Use only import type so no server packages are pulled in:

src/db/__mocks__/client.ts
import type { drizzle } from 'drizzle-orm/postgres-js';
import type * as schema from '../schema';
 
export const db = new Proxy({} as ReturnType<typeof drizzle<typeof schema>>, {
  get: () => () => Promise.resolve([]),
});

Why a __mocks__ file instead of automocking?

Storybook's automocking replaces functions but still evaluates the original module and its imports. For modules that import postgres, pg, or other Node.js-only packages, the original module must never be evaluated, because it would crash the browser. A __mocks__ file is the only approach that completely prevents evaluation of the original module and its dependency chain.

Identifying what to mock

Errors like does not provide an export named 'default' or AsyncLocalStorage is not defined mean a server-only module reached the browser.

The fix is to mock the server module itself, not the component or route that uses it. For example, if Dashboard.tsx imports ~/auth/session, and ~/auth/session imports ~/db/client, and ~/db/client imports postgres โ€” mock ~/db/client. The Node.js dependency (postgres) is the smoking gun; mock the closest module to it that you control.

To find that module, walk the error stack trace from top to bottom and stop at the first import you wrote yourself. Then add a __mocks__ file for it.

Two cases where you do not need a mock:

  • The module is from @tanstack/* โ€” already handled by the framework preset. Make sure you are on the latest @storybook/tanstack-react.
  • The module only imports createServerFn โ€” already mocked. The error is coming from another import in the same file.

TanStack Query

You can use this framework together with TanStack Query to provide a working QueryClient in Storybook and seed query data per story.

Project setup

TanStack Query is not automatically set up. The recommended approach is to create a single QueryClient in your preview file, clear it between stories via loaders, and share the same instance through both parameters.tanstack.router.context and a QueryClientProvider decorator.

.storybook/preview.tsx
import { type QueryClient, QueryClientProvider } from '@tanstack/react-query';
 
// ๐Ÿ‘‡ Create a new QueryClient
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      retry: false,
      staleTime: Infinity,
    },
  },
});
 
export default {
  loaders: [
    // ๐Ÿ‘‡ Clear the cache between stories so each story starts fresh
    () => {
      queryClient.clear();
    },
  ],
  parameters: {
    tanstack: {
      router: {
        // ๐Ÿ‘‡ Make queryClient available to route loaders via ctx.context.queryClient
        context: { queryClient },
      },
    },
  },
  decorators: [
    (Story) => (
      // ๐Ÿ‘‡ Provide the QueryClient to all stories
      <QueryClientProvider client={queryClient}>
        <Story />
      </QueryClientProvider>
    ),
  ],
};

Seeding query data per story

In individual stories, use loaders to call setQueryData on the shared QueryClient before the component renders. Access it from parameters.tanstack.router.context:

Navbar.stories.ts
import type { Meta, StoryObj } from '@storybook/tanstack-react';
import type { QueryClient } from '@tanstack/react-query';
 
import { Navbar } from './Navbar';
 
const meta = {
  component: Navbar,
} satisfies Meta<typeof Navbar>;
 
export default meta;
type Story = StoryObj<typeof meta>;
 
export const Default: Story = {};
 
export const LoggedIn: Story = {
  loaders: [
    async ({ parameters }) => {
      const qc: QueryClient = parameters.tanstack?.router?.context?.queryClient;
      qc?.setQueryData(['currentUser'], {
        id: 'user-1',
        name: 'Ada Lovelace',
      });
    },
  ],
};

FAQ

How do I migrate from the react-vite framework?

Automatic migration

Storybook provides a migration tool for migrating to this framework from the React (Vite) framework, @storybook/react-vite. To migrate, run this command:

npx storybook automigrate react-vite-to-tanstack-react

This automigration tool performs the following actions:

  1. Updates package.json files to replace @storybook/react-vite with @storybook/tanstack-react.
  2. Updates .storybook/main.js|ts to change the framework property (works with both regular and CSF factories defineMain configs).
  3. Scans and updates import statements that reference @storybook/react-vite in your story files and Storybook configuration files (including @storybook/react-vite/node used by CSF factories).
  4. Detects manual TanStack Router decorators in .storybook/preview.*, the rest of .storybook/, and any *.stories.* file. When one is found, the CLI offers to copy a ready-to-paste AI prompt to your clipboard that walks an AI assistant through removing the now-redundant decorator.

@storybook/tanstack-react already wraps every story in a TanStack Router automatically, so any manual RouterProvider / createRouter / createMemoryHistory / createRootRoute decorator should be removed after running the automigration. For stories that need a specific route, use parameters.tanstack.router instead.

Manual migration

First, install the framework:

npm install --save-dev @storybook/tanstack-react

Then, update your .storybook/main.js|ts to change the framework property:

.storybook/main.ts
- import type { StorybookConfig } from '@storybook/react-vite';
+ import type { StorybookConfig } from '@storybook/tanstack-react';
 
const config: StorybookConfig = {
  // ...
-  framework: '@storybook/react-vite',
+  framework: '@storybook/tanstack-react',
};
 
export default config;

Then similarly update your .storybook/preview.* to import from @storybook/tanstack-react:

.storybook/preview.tsx
- import type { Preview } from '@storybook/react-vite';
+ import type { Preview } from '@storybook/tanstack-react';
 
const preview: Preview = {
  //...
};
 
export default preview;

@storybook/tanstack-react already wraps every story in a TanStack Router automatically, so any manual RouterProvider / createRouter / createMemoryHistory / createRootRoute decorator should be removed after running the automigration. For stories that need a specific route, use parameters.tanstack.router instead.

When should I use @storybook/tanstack-react instead of @storybook/react-vite?

Use @storybook/tanstack-react when your components rely on TanStack Router or TanStack Start APIs and you want Storybook to provide router context, typed route parameters, automatic router mocking, and mocked TanStack Start server-function behavior.

Use @storybook/react-vite when your app is a standard React and Vite project without TanStack Router.

My styles are missing in Storybook

Import your application CSS in .storybook/preview.* so it is bundled with the preview:

.storybook/preview.tsx
import '../src/styles/app.css';

For more information, see the styling documentation.

How do I provide React context providers (e.g. theme, toast, auth) to all stories?

Add project-level decorators to apply providers to all stories.

You can also add component-level decorators to apply providers to all stories for a specific component, or story-level decorators to apply providers to a single story.

Does @storybook/tanstack-react support React Server Components?

No. @storybook/tanstack-react runs stories in the browser using a memory-backed router. React Server Components require a server runtime and are not supported. If your component is a Server Component, extract the client-side parts into a Client Component and write stories for that instead.

Story fails to render with an error about modules not providing a default export

This usually means a server-only module is being imported in the browser. Check the error stack trace to find the module and add a Storybook mock for it as described in Handling server-only dependencies.

API

Modules

The package exports these additional modules:

@storybook/tanstack-react/react-router

TanStack Router-compatible mock implementations used by the framework to provide router behavior in stories. Import from this module when you need direct access to the mock APIs (for example, to assert against navigation spies in tests).

@storybook/tanstack-react/start

TanStack Start-compatible mock implementations, including a mocked createServerFn() implementation. Import from this module when a story or test needs to interact directly with the Start mock layer.

Options

You can pass an options object for additional configuration if needed:

.storybook/main.ts
import type { StorybookConfig } from '@storybook/tanstack-react';
 
const config: StorybookConfig = {
  framework: {
    name: '@storybook/tanstack-react',
    options: {
      builder: {
        // Vite builder options
      },
    },
  },
};
 
export default config;

The available options are:

builder

Type: Record<string, any>

Configure options for the framework's builder. Available options can be found in the Vite builder docs.

Parameters

This framework contributes the following parameters to Storybook under the tanstack.router namespace:

When route is supplied as a plain object, it may also include TanStack route options such as head, search, and params.parse.

context

Type: Record<string, unknown>

Router context values injected into the story router.

params

Type: ResolveParams<Path>

Interpolates route params into the current path. When route is a typed file route, the type is constrained to the param names declared in that route's path (for example, { id: string } for /$id).

path

Type: string

Sets the initial URL path for the story router.

query

Type: Record<string, unknown>

Appends search params to the initial URL.

route

Type: AnyRoute | route options object

Supplies a route instance directly or creates a temporary story route from route options. Storybook extracts the route's React component automatically from the route.

routeOverrides

Type: Partial<Record<string, RouteOverrideOptions>>

Per-route overrides keyed by route ID, applied to the story's route and root route. Use '__root__' to target the root route. Each entry can override loader, beforeLoad, validateSearch, loaderDeps, and context.

useRouterContext

Type: ({ storyContext }) => RouterContext

Dynamically computes the router context from the story context. Use this when the router context depends on values that are already available in the story (for example, a QueryClient that is loaded by a story loader).

parameters: {
  tanstack: {
    router: {
      useRouterContext: ({ storyContext }) => ({
        queryClient: storyContext.loaded.queryClient,
      }),
    },
  },
}

This is an alternative to context for cases where the router context needs a React context provider (e.g., TanStack Query's QueryClientProvider) that must be rendered in the story before the context value can be accessed.