Render a single React Native component in isolation, in your app's real provider shell, with chosen props/state, without booting the rest of the app. Uses Storybook-compatible CSF (*.stories.tsx) with zero setup (no .rnstorybook/, no codegen manifest, no Metro wrapper), and runs on both surfaces: the iOS/Android simulator (true native) and a browser (via react-native-web).
The differentiator: your real AppShell wraps every story (single source of truth, can't drift), and CSF decorators run inside it. Edit a story's args live on-device with the built-in controls panel.
pnpm add -D component-previewer # or npm i -D / yarn add -DPeer dependencies: react, react-native.
- Expose your real provider shell as one component (theme, query client, i18n, safe-area, …):
// AppProviders.tsx export function AppProviders({ children }) { /* your real providers */ }
- Write CSF stories (
*.stories.tsx), the exact Storybook format, no import from this tool needed:// Button.stories.tsx import { Button } from './Button'; export default { title: 'Button', component: Button, args: { label: 'Press me' } }; export const Primary = { args: { label: 'Buy now' } }; export const Disabled = { args: { disabled: true } }; // optional per-story providers run INSIDE your real shell: // export const Boxed = { args: {...}, decorators: [(Story) => <View>…<Story/></View>] };
- Wire the boot flag at your app root (~10 lines). Discovery is zero-codegen:
On a pure-Vite host, use
import { Previewer, fromRequireContext } from 'component-previewer'; const ctx = require.context('./', true, /\.stories\.tsx$/); // Metro / native + Expo web const PREVIEW = process.env.EXPO_PUBLIC_PREVIEW === '1'; const Shell = ({ children }) => <AppProviders>{children}</AppProviders>; export default function App() { return PREVIEW ? <Previewer stories={fromRequireContext(ctx)} shell={Shell} /> : <YourApp />; }
fromGlob(import.meta.glob('./**/*.stories.tsx', { eager: true }))instead offromRequireContext.
Open any story and tap Controls to edit its args on-device and re-render live (the Storybook-controls equivalent). Control kinds are inferred from the arg values with zero config:
| value type | control |
|---|---|
string |
text field |
number |
numeric field |
boolean |
switch |
Functions, objects, arrays, null/undefined are skipped (passed through untouched). For a dropdown, declare optional CSF argTypes on a story (also used for ranges):
export default {
title: 'Badge',
component: Badge,
args: { tone: 'neutral' },
argTypes: { tone: { control: 'select', options: ['neutral', 'success', 'danger'] } },
};
// 'range' is supported too and renders as a clamped number field:
// argTypes: { size: { control: 'range', min: 16, max: 256, step: 8 } }A Reset button restores the story's declared args. Edits never leak between stories.
The repo ships an Expo example app.
Native (simulator / device):
cd example
EXPO_PUBLIC_PREVIEW=1 npx expo start # press i, or scan with Expo GoBoots into a searchable picker of discovered stories; selecting one renders it natively inside the real shell. This is the surface that shows Skia/glass/native modules (the example's BlurCard story renders a real native blur).
Browser (no simulator):
EXPO_PUBLIC_PREVIEW=1 npx expo start --webSame picker via react-native-web. Native modules can't render here (documented cap).
Without the flag, the app boots its normal screen instead. Deep-link straight into one story with EXPO_PUBLIC_PREVIEW_STORY=button--primary.
fromRequireContext(ctx)/fromGlob(glob)→StoryEntry[]— zero-codegen discovery.<Previewer stories={…} shell={…} initialStoryId? />— the picker + stage UI;initialStoryId(orEXPO_PUBLIC_PREVIEW_STORY) deep-links straight into one story.inferControls(args, argTypes?)→Control[]— pure; the control descriptors that drive the on-device panel.composeStory(entry, Shell?, args?)—Shell(outer) > decorators (inner) > story; pure. Passargsto render an override (the live-edit path).parseCsfModule(id, module)→StoryEntry[]— parse one CSF module to entries.
Exported types: StoryEntry, Meta, Story, Args, ArgType, ArgTypes, ControlKind, Control, Decorator, ShellComponent, RequireContext, GlobModules, CsfModule.
This is a pnpm workspace: the example/ app consumes the library source via Metro (bob's metro-config + the source export condition), so editing src/ hot-reloads in the example, no rebuild or repack.
pnpm install # once — links the workspace
pnpm lint # eslint (typescript-eslint)
pnpm typecheck # tsc --noEmit
pnpm test # vitest (CSF parsing, discovery, shell compose, control inference)
pnpm build # react-native-builder-bob → lib/module (ESM) + lib/typescript (.d.ts)Build/packaging: react-native-builder-bob (ESM module + typescript targets), exports map with a source condition, MIT licensed. See CONTRIBUTING.md.
- Core: CSF parsing, zero-codegen discovery (
require.context/import.meta.glob), registry, real-shell compose. Pure and unit-tested. - Native backend: boot-flag entry swap + in-app picker + real-shell render + deep-link, verified on the iOS simulator (including a native
expo-blurstory). - Web backend: renders the same CSF stories via react-native-web (native-module cap documented).
- Live props controls: value-inferred text/number/boolean + CSF
argTypesselect/range, on-device, live. - Roadmap: TS-type-driven prop controls (auto-infer from prop types), VS Code extension, more CSF decorator edge cases.