Storybook React Native: CSF3 setup guide for Expo projects

react_native_storybook

Storybook React Native setups fail less often because of Metro config errors and more because teams treat it like Storybook for web: same addons, same story format, same expectations. Component Story Format 3 changed how stories are written, and Expo's routing model changes where Storybook lives in your app.

Get those two things wrong and you end up with a Storybook instance that renders but never gets used for real QA. Here's how to set it up correctly, from a fresh Expo project through to running stories as automated tests.

Storybook React Native setup: What you need to know

Most Storybook React Native setups stall at the same fork: stories written in Component Story Format 3 (CSF3) versus legacy formats, and Expo driving the Metro bundler config versus Metro running standalone. Get those two decisions wrong and every story needs a rewrite.

Storybook itself pulls roughly 18.2M weekly npm downloads, and @storybook/react-native around 681,000 (npm registry download stats), a scale that reflects args-based CSF3 stories overtaking legacy formats as the default across the ecosystem. What follows covers Metro bundler configuration, Expo Router integration, and the entry-point swap that keeps Storybook out of production app builds.

New project vs existing project: Two installation paths

Setting up Storybook in React Native splits into two paths: a fresh Expo scaffold where the CLI wires everything for you, and an existing app where you're hand-editing a Metro bundler configuration that already has opinions. The second path is where most application setups go wrong.

For a new project, the flow is close to zero-friction by name and in practice:

npx create-expo-app@latest my-app
cd my-app
yarn add -D @storybook/react-native @storybook/addon-ondevice-controls @storybook/addon-ondevice-actions
npx storybook init

That generates .storybook/main.ts, .storybook/preview.tsx, and storybook.requires.ts, and registers the Controls addon out of the box. Run yarn ios or yarn android and the on-device Storybook boots against the same Metro process as your app.

For an existing project, Yarn installs the same packages, but npx storybook init won't safely rewrite a Metro config that already has custom transformers or Expo Router integration wired in. You edit metro.config.js by hand:

const config = withStorybook(getDefaultConfig(__dirname), {
 enabled: process.env.STORYBOOK === 'true',
});

That enabled flag is the entry-point swap: flip an environment variable and Metro serves Storybook instead of your app, which is how teams keep a production build and a Storybook build from fighting over the same bundler.

On an existing React Native codebase of meaningful size, budget half a day for this step, not the ten minutes the new-project path takes. If integrating Storybook into a large legacy codebase proves too time-consuming for your in-house engineers, partnering with an expert React Native team can help keep the migration on schedule.

Writing stories with CSF3 and args (No storiesOf)

Component Story Format 3 (CSF3) replaced storiesOf as the default Storybook authoring pattern back in Storybook 6.4, and by Storybook 9 the legacy API is gone from the React Native template entirely. If you're porting an older RN app, this is the rewrite you can't skip.

The old pattern registered stories imperatively, one call per variant:

storiesOf('Button', module).add('default', () => <Button label="Save" />).add('disabled', () => <Button label="Save" disabled />);

CSF3 replaces that with a default export plus args-based stories, each one a plain object instead of a render function:

const meta = {
 component: Button,
 args: { label: 'Save' },
};
export default meta;

export const Default = {};
export const Disabled = { args: { disabled: true } };

The practical gain shows up in the Controls addon: args are serializable, so Controls can generate a live prop editor without you writing knobs by hand. On a native device, that editor renders as an in-app panel; on the React Native Web build of the same story file, it's the standard web-rendered Controls UI. Same story, same args object, two renderers that now work in parallel.

Decorators still wrap the component the same way they did under storiesOf, but they're declared once in meta instead of chained per story. According to Storybook's official CSF3 migration guide, this cuts duplicate boilerplate across story files by consolidating shared config into the default export rather than repeating it per add call.

Expo Router screens compose the same way: wrap the routed component in a story, pass route params as args, and Metro resolves it like any other module.

Configuring Storybook for Expo: Metro and the Storybook route

Configuring Storybook for Expo comes down to two files: a Metro bundler configuration that teaches the bundler to resolve.storybook on top of your normal Expo Router tree, and a dedicated Expo Router route that swaps your app's root component for the Storybook UI at runtime.

The Metro side is a few lines on top of getDefaultConfig:

// metro.config.js
const { getDefaultConfig } = require('expo/metro-config');
const { withStorybook } = require('@storybook/react-native/metro/withStorybook');

const config = getDefaultConfig(__dirname);

module.exports = withStorybook(config, {
 enabled: process.env.STORYBOOK_ENABLED === 'true',
 configPath: require('path').resolve(__dirname, './.storybook'),
});

WithStorybook wraps the default config rather than replacing it, so Metro still resolves your normal app imports. The enabled flag is the entry-point swap: flip it off and Metro strips the Storybook require graph entirely from production builds, so App Store and Play Store binaries carry zero Storybook weight.

Gate it behind an env var checked in CI, not a hand-edited boolean, since it's the one line teams forget to flip back before a release build.

On the Expo Router side, add a route (app/storybook.tsx) that exports the generated .storybook entry as its default, then launch it with npx expo start and navigate to that route on iOS, Android, or web. Running the same route through React Native Web gives you a second, browser-rendered Storybook instance from the identical story files, no separate config duplicated.

That parallel setup, one Metro-bundled for on-device QA and one web-rendered for design review, is what actually earns Storybook's adoption cost, according to the storybookjs/react-native README.

Essential addons: Controls, Actions, Viewport, and a11y

Controls, Actions, Viewport, and Accessibility (a11y) are the four addons worth installing on every React Native and Expo project by default, and Controls does the heaviest lifting. It replaces the deprecated knobs addon entirely, knobs required imperative knob calls inside a story body, while Controls reads types straight off your CSF3 args object and generates the UI automatically, no extra config beyond a control type override for edge cases like color pickers or native-only enum props.

Actions logs every prop callback fired during interaction, which matters more on native than on web: a touch gesture on iOS or Android doesn't leave a console trail the way a browser click does, so Actions is often the only signal that an onPress actually ran during manual QA in Storybook.

Viewport swaps device frames on the fly, letting you check a component against iPhone, Pixel, and tablet presets without rebuilding the app. On React Native Web, Viewport also doubles as your only real proxy for responsive breakpoints, since Metro's native renderer has no browser chrome to resize.

Accessibility (a11y) runs automated audits against each story and is the addon most React Native teams skip, wrongly. According to Storybook's addon documentation, the a11y addon runs axe-core checks in the browser-rendered instance and flags contrast and labeling issues before they reach a device build.

Storybook React Native vs Storybook for React Native Web

Storybook React Native renders each story as a real native screen through Metro, the same bundler that ships your iOS and Android builds. Storybook for React Native Web renders the identical CSF3 stories in a browser using React Native Web, swapping native primitives for their web equivalents.

Storybook React Native (on-device) Storybook + React Native Web
Bundler Metro (or Re.Pack for RN 0.73+) Webpack / Vite via @storybook/react-webpack5
Renders True native views, gestures, native modules DOM approximation, no native module access
Best for Final visual QA, platform-specific bugs Fast design review, PR previews, Chromatic diffs
CI cost Needs a simulator or device farm Runs headless in seconds

Re.Pack matters here because it lets teams keep Metro's Fast Refresh behavior while swapping in a Webpack-compatible module federation setup for web builds, which is the entry-point swap most Expo Router projects need when they maintain both targets from one story file.

A common pattern: run both in parallel, a web-rendered instance in CI for every pull request, and a Metro-based on-device instance for release candidates. That split keeps the default npm run storybook fast while still catching native-only regressions. @storybook/react used in 1,372 npm projects vs @storybook/react-native in only 56 (npm registry package pages, 2025).

This dual-pipeline approach is common among teams that work with an experienced React Native development partner to ship both web and native builds from a single codebase.

Turning stories into automated tests with Storybook test-runner

Storybook test-runner turns every CSF3 story into an automated Jest test, replaying each story's play function through Playwright and failing the build the moment a component's rendered output drifts. Because the runner drives a browser, it only works against the React Native Web instance, not the on-device Metro build, whether that build comes from a bare React Native app or an Expo project.

Interaction testing lives inside the story itself, which you can import into your testing framework. Add a play function next to your args, and the default test framework clicks, types, and asserts the same way a QA engineer would in a manual pass:

const meta: Meta<typeof Button> = { component: Button };
export default meta;

export const Filled: Story = {
 args: { label: 'Submit' },
 play: async ({ canvasElement }) => {
 const canvas = within(canvasElement);
 await userEvent.click(canvas.getByRole('button'));
 },
};

Run it against a live Storybook instance by passing a connection string.

$ npx test-storybook --url http://localhost:6006
 PASS src/Button.stories.tsx
 ✓ Filled (312 ms)

Pass Playwright options through the test-runner config the same way you'd wire up any other scripts entry in your Storybook config, and every pull request gets interaction coverage without touching a simulator.

The catch: test-runner exercises the web-rendered copy of your app, not the native one. A gesture handler that misbehaves on Android or a shadow that renders wrong on iOS still needs a native pass. Treat test-runner as the first gate, not the only one.

Sharing builds: TestFlight, Android, and web publishing

Sharing a build for review comes down to three paths, and the right one depends on who is reviewing and how often. For design and product sign-off, publish React Native Web to a static host: anyone opens a URL, no build step required. For device-specific QA, ship an actual on-device Storybook build through TestFlight or an Android APK.

You can see entry-point swapping controls which app boots. Point index.js at the Storybook config instead of the app root for a review build, then swap it back before submitting to the App Store:

const AppEntryPoint = process.env.STORYBOOK_ENABLED === 'true' ? './.storybook': './App';
export default AppEntryPoint;

That swap is how a Storybook-only binary reaches TestFlight without touching the production target. Android reviewers get the same build pipeline, entry swapped, APK signed as normal. For Expo projects, run the same swap through Expo Router's entry file alongside metro.config.js, then push through EAS Submit to TestFlight.

Running parallel Storybook instances, one native (Metro, for TestFlight and Android) and one web (React Native Web, via webpack or Vite), covers both audiences without maintaining two component libraries.

Decision rule: web for review velocity, native for pixel fidelity, both running in parallel when a distributed team needs each.

Hiding Storybook from production and running parallel instances

Keeping Storybook out of production means gating the entry point, not deleting the config. Metro bundler configuration reads an environment flag (STORYBOOK=true) at build time and swaps index.js for a Storybook-aware entry, so one expo run command produces either the customer app or the component-only bundle from the same repo.

Running React Native and web instances in parallel doesn't require a second codebase. Point one Metro config at the native target and let React Native Web serve the second instance from the same stories directory. Both read identical CSF3 files, so a story written once renders on iOS, Android, and web without a copy-paste fork.

Expo Router integration adds one wrinkle: route-based screens expect a navigation container that Storybook's on-device renderer doesn't supply. Wrap those components in a thin default export rather than importing the raw route file as a story.

For teams sizing this up against a plain in-app QA flow, @storybook/react-native's roughly 681,000 weekly npm downloads is a reasonable proxy for how far the pattern has moved past early adopters.

Advantages and disadvantages of Storybook in React Native

Storybook in React Native buys speed on component QA and costs you a second build to maintain. The tradeoff is worth naming plainly rather than glossing over.

Advantage Disadvantage
CSF3 args-based stories reproduce loading, error, and empty states in seconds, no app navigation required Metro bundler configuration needs its own entry point; drift between the Storybook config and the app config causes "works in Storybook, breaks in app" bugs
Controls addon lets a reviewer flip props live, no code edit, no rebuild Native modules, camera, biometrics, Android/iOS permission dialogs, don't render meaningfully off-device; Storybook shows the mock, not the real prompt
One component renders on native and React Native Web, catching layout drift between platforms before a PR merges Isolated stories miss integration bugs: navigation stack state, deep-link params, cross-screen data sync
Storybook test-runner turns every CSF3 story into an automated visual regression check in CI A second bundler target adds a maintenance line item most teams underestimate at adoption time

According to npm's registry download trends for, the package has seen consistent year-over-year download growth since Storybook 7 shipped native support, which tracks with broader React Native teams treating component isolation as a default step rather than an experiment.

The pattern holds broadly: Storybook shortens the loop for pure UI review, but a release still needs an in-app pass on real devices before it ships.

FAQ: Storybook React Native setup and testing

How do I set up Storybook with Expo?

Run npx storybook@latest init inside an Expo project, including ones built with Expo Router; the CLI detects your Expo config and wires Metro automatically. It installs the on-device UI and adds a storybook-enabled script to package.json. Use this path when stories need to run inside the real app, not a browser.

Is Storybook React Native different from Storybook React Native Web?

Yes. Storybook React Native renders on-device via Metro, while Storybook React Native Web renders the same components in a browser via webpack or Vite. Stories and args stay identical; only the bundler and rendering target change. Pick Web when you want faster CI screenshot testing without a simulator.

How do I test React Native components with Storybook?

Write CSF3 stories with args for each component state, then browse them on-device in the Expo app or run Storybook test-runner against a web-rendered build in CI. The test-runner executes Jest-based checks per story automatically. This catches regressions before a component ever reaches a real screen.

What addons should I install for Storybook React Native?

Install the Controls addon for live prop editing, the Actions addon for event logging, and Storybook test-runner for automated CI checks. Accessibility and viewport addons help too on component-heavy screens. Skip addons built purely for web DOM inspection since they don't apply on-device.

How do I publish Storybook React Native to TestFlight?

Build a separate Expo binary with Storybook set as the entry point, then submit that build to TestFlight like any normal iOS app. Entry-point swapping keeps the production app's config untouched while QA reviews components in isolation. Revert the entry point before shipping the real release.

What's the Metro config for Storybook React Native?

The Storybook CLI generates .storybook/metro.config.js, which wraps getDefaultConfig from Expo's Metro config, for example const config = getDefaultConfig(__dirname). Your root metro.config.js still points to the normal app. Keep both files in sync manually; drift here is the most common Storybook React Native bug.

Can I run parallel Storybook instances for React Native and web?

Yes, run parallel Storybook instances for React Native and React Native Web with separate ports and config directories, one on Metro and one on webpack or Vite. Many teams run storybook dev for Web alongside the Expo build for fast browser review. This speeds design review without touching a simulator.

What are the advantages and disadvantages of Storybook?

Storybook speeds up component QA with args-based stories that reproduce edge-case states in seconds, but it adds a second build to maintain. Metro config can drift from the app's main config over time. Adoption keeps climbing regardless, evidence the QA speed outweighs the maintenance cost for most teams.

Get help setting up Storybook in your React Native app

Wiring Storybook into an Expo Router app, keeping Metro config sane across a React Native and React Native Web target, and deciding whether to run stories on-device or in a browser is not a one-afternoon task once you factor in CI. Teams that get this config right early avoid rebuilding their story architecture after the app has scaled.

If your team wants a second opinion on config, entry-point swapping, or test-runner coverage, talk to our team.

We're Netguru

At Netguru we specialize in designing, building, shipping and scaling beautiful, usable products with blazing-fast efficiency.

Let's talk business