diff --git a/.changeset/flat-impalas-press.md b/.changeset/flat-impalas-press.md
new file mode 100644
index 0000000000..469572c3bd
--- /dev/null
+++ b/.changeset/flat-impalas-press.md
@@ -0,0 +1,5 @@
+---
+'@storybook/react-native': patch
+---
+
+feat: adds csf next support
diff --git a/.gitignore b/.gitignore
index 3362676108..3c9e66a0a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,7 +10,6 @@ dist
.cache
junit.xml
coverage/
-*.lerna_backup
build
/**/LICENSE
docs/public
diff --git a/.prettierignore b/.prettierignore
index eaf48a1e8f..3ea09995e2 100644
--- a/.prettierignore
+++ b/.prettierignore
@@ -3,6 +3,7 @@ pnpm-lock.yaml
dist/
examples/expo-example/.expo
examples/expo-example/.rnstorybook/storybook.requires.ts
+examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts
docs/.docusaurus
docs/build
.claude/
diff --git a/CLAUDE.md b/CLAUDE.md
index ee8cb8e3d3..ce02846438 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,97 +1,53 @@
This file provides guidance to agents when working with code in this repository.
-Always check first if the react-native-best-practices skill can be used
+check available mcp and skills
-## Development Commands
+use pnpm for commands, check package.json for scripts
-```bash
-# Initial Setup
-pnpm install
-pnpm build
-
-# Development
-pnpm dev # Watch all packages for changes
-pnpm example # Run the expo example app with Storybook
-
-# Story Generation
-pnpm -F expo-example storybook-generate # Regenerate storybook.requires.ts
+## On-Device Testing Tools
-# Testing
-pnpm test # Run unit tests across all packages
-pnpm test:ci # Run tests in CI mode
+- use agent-device to control a simulator `agent-device --help`
+- use rn-logs to get metro logs `rn-logs logs --help`
+- use the storybook mcp to select stories and get story list
-# Code Quality
-pnpm lint # Run ESLint across the codebase
-pnpm format:check # Check Prettier formatting
-pnpm format:fix # Auto-fix Prettier formatting
+use curl to send events to channel server, such as to update the args:
-# Documentation (from docs/ directory)
-cd docs
-pnpm start # Start development server
-pnpm build # Build documentation
-pnpm serve # Serve built documentation
+```sh
+curl -X POST http://localhost:7007/send-event \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "updateStoryArgs",
+ "args": [{
+ "storyId": "controlexamples-controlexample--example",
+ "updatedArgs": { "name": "Alice", "age": 25 }
+ }]
+ }'
```
-## Architecture Overview
-
-**pnpm workspaces monorepo** managed by Lerna containing React Native Storybook packages.
-
-### Packages
-
-**Apps**
-
-- examples/expo-example - Expo example app showcasing Storybook
-- docs - Documentation site for Storybook React Native
-
-**Core:**
-
-- `@storybook/react-native` - Main package providing Storybook functionality
-- `@storybook/react-native-ui` - Full UI components for on-device Storybook
-- `@storybook/react-native-ui-lite` - Lightweight UI components
-- `@storybook/react-native-ui-common` - Shared UI components
-- `@storybook/react-native-theming` - Theming utilities
-
-**On-Device Addons:**
+### agent-device (iOS/Android Simulator Control)
-- `@storybook/addon-ondevice-actions` - Log component interactions
-- `@storybook/addon-ondevice-backgrounds` - Change story backgrounds
-- `@storybook/addon-ondevice-controls` - Dynamically edit component props
-- `@storybook/addon-ondevice-notes` - Add markdown documentation to stories
-
-### Build System & Metro Configuration
-
-- Uses **tsup** for TypeScript compilation (ES2022, CommonJS output)
-- Each package has its own `tsup.config.ts`
-- `pnpm prepare` in a package builds it
-
-The `withStorybook` Metro wrapper (for Metro-based projects):
-
-- Enables `unstable_allowRequireContext` for dynamic story imports
-- Automatically generates `storybook.requires.ts` file
-- Optional WebSocket server for remote control
-- Can be conditionally enabled/disabled via `enabled` option
-- Supports `liteMode` for reduced bundle size
-
-The `StorybookPlugin` (for Re.Pack/Rspack/Webpack projects):
+```bash
+agent-device open host.exp.Exponent --relaunch # Relaunch Expo Go
+agent-device snapshot -c # Take accessibility snapshot (shows @refs)
+agent-device click @e14 # Click element by ref from snapshot
+agent-device find "Press me" click # Find text and click it
+```
-- Alternative to `withStorybook` for non-Metro bundlers
-- Imported from `@storybook/react-native/repack/withStorybook`
-- Requires `enablePackageExports: true` in rspack resolve options
-- Uses `DefinePlugin` for build-time `STORYBOOK_ENABLED` constant
-- No `require.context` configuration needed (rspack handles it natively)
-- Same options as `withStorybook` (enabled, configPath, useJs, docTools, liteMode, websockets)
+After relaunching, you need to press the "Expo Example" to go to it.
-### Testing
+### rn-logs (React Native Log Streaming)
-- Uses **jest** with `jest-expo` preset
-- `universal-test-renderer` for portable story testing
-- Story generation tested with Node's native test runner
+```bash
+rn-logs apps # List running apps
+rn-logs logs --app "host.exp.Exponent" # Stream logs from Expo Go
+```
-### Key Concepts
+## Key Concepts
1. **CSF (Component Story Format)** - Standard story syntax
2. **On-device UI** - Native UI that runs directly on mobile devices
-3. **Story requires generation** - Automatic generation of story imports via Metro
-4. **Portable stories** - Reuse stories in unit tests
-5. **WebSocket support** - Remote control stories from external devices
-6. **Lite mode** - Alternative UI without heavy dependencies (reanimated, etc.)
+3. **Story requires generation** - Automatic generation of story imports via Metro (`storybook.requires.ts`)
+4. **Portable stories** - Reuse stories in unit tests via `universal-test-renderer`
+5. web storybook codebase can be referenced and likely can be found at ../storybook (from root)
+
+additional information in docs folder and readme file
diff --git a/ROADMAP.md b/ROADMAP.md
index 7507a05a3f..4e09810ff0 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -2,12 +2,12 @@
For v11:
-- csf factories
-- backgrounds with globals
-- addons into core
-- make lite ui the default and remove dependencies from controls
+- [x] csf factories
+- [x] backgrounds with globals
+- [ ] addons into core
+- [ ] make lite ui the default and remove dependencies from controls
stretch goals:
-- simple docs implementation
-- dev tooling like vscode extension and rn dev tools integration
+- [ ] simple docs implementation
+- [x] dev tooling like vscode extension and rn dev tools integration
diff --git a/examples/expo-example/.rnstorybook-nofactories/index.tsx b/examples/expo-example/.rnstorybook-nofactories/index.tsx
new file mode 100644
index 0000000000..be264b7cb0
--- /dev/null
+++ b/examples/expo-example/.rnstorybook-nofactories/index.tsx
@@ -0,0 +1,41 @@
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import { LiteUI } from '@storybook/react-native-ui-lite';
+import { StatusBar, View } from 'react-native';
+import { SafeAreaView, SafeAreaProvider } from 'react-native-safe-area-context';
+import { view } from './storybook.requires';
+
+const isScreenshotTesting = process.env.EXPO_PUBLIC_SCREENSHOT_TESTING === 'true';
+const isLiteUI = process.env.EXPO_PUBLIC_LITE_UI === 'true';
+
+const StorybookUIRoot = view.getStorybookUI({
+ shouldPersistSelection: true,
+ storage: {
+ getItem: AsyncStorage.getItem,
+ setItem: AsyncStorage.setItem,
+ },
+ enableWebsockets: true,
+
+ CustomUIComponent: isScreenshotTesting
+ ? ({ children, story }) => {
+ return (
+
+
+
+
+ {children}
+
+
+
+ );
+ }
+ : isLiteUI
+ ? LiteUI
+ : undefined,
+});
+
+export default StorybookUIRoot;
diff --git a/examples/expo-example/.rnstorybook-nofactories/main.ts b/examples/expo-example/.rnstorybook-nofactories/main.ts
new file mode 100644
index 0000000000..d524e50c97
--- /dev/null
+++ b/examples/expo-example/.rnstorybook-nofactories/main.ts
@@ -0,0 +1,28 @@
+import type { StorybookConfig } from '@storybook/react-native';
+
+const main: StorybookConfig = {
+ stories: [
+ '../components/**/!(*.factories).stories.?(ts|tsx|js|jsx)',
+ '../other_components/**/!(*.factories).stories.?(ts|tsx|js|jsx)',
+ {
+ directory: '../../../packages/react-native-ui',
+ titlePrefix: 'react-native-ui',
+ files: '**/!(*.factories).stories.?(ts|tsx|js|jsx)',
+ },
+ ],
+ addons: [
+ '@storybook/addon-ondevice-controls',
+ '@storybook/addon-ondevice-actions',
+ '@storybook/addon-ondevice-notes',
+ 'storybook-addon-deep-controls',
+ ],
+ reactNative: {
+ playFn: false,
+ },
+ features: {
+ ondeviceBackgrounds: true,
+ },
+ framework: '@storybook/react-native',
+};
+
+export default main;
diff --git a/examples/expo-example/.rnstorybook-nofactories/preview.tsx b/examples/expo-example/.rnstorybook-nofactories/preview.tsx
new file mode 100644
index 0000000000..37d6c0ccaf
--- /dev/null
+++ b/examples/expo-example/.rnstorybook-nofactories/preview.tsx
@@ -0,0 +1,37 @@
+import { Appearance } from 'react-native';
+
+const preview = {
+ parameters: {
+ actions: { argTypesRegex: '^on[A-Z].*' },
+ controls: {
+ matchers: {
+ color: /(background|color)$/i,
+ date: /Date$/,
+ },
+ },
+ options: {
+ storySort: {
+ method: 'alphabetical' as const,
+ includeNames: true,
+ order: ['ControlExamples', ['ControlExample'], 'InteractionExample', 'DeepControls'],
+ },
+ },
+ hideFullScreenButton: false,
+ noSafeArea: false,
+ my_param: 'anything',
+ layout: 'padded',
+ storybookUIVisibility: 'visible',
+ backgrounds: {
+ options: {
+ dark: { name: 'dark', value: '#333' },
+ light: { name: 'plain', value: '#fff' },
+ app: { name: 'app', value: '#eeeeee' },
+ },
+ },
+ },
+ initialGlobals: {
+ backgrounds: { value: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain' },
+ },
+};
+
+export default preview;
diff --git a/examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts b/examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts
new file mode 100644
index 0000000000..19fd2c4f1c
--- /dev/null
+++ b/examples/expo-example/.rnstorybook-nofactories/storybook.requires.ts
@@ -0,0 +1,82 @@
+/* do not change this file, it is auto generated by storybook. */
+///
+import { start, updateView, View, type Features } from '@storybook/react-native';
+
+import "@storybook/addon-ondevice-controls/register";
+import "@storybook/addon-ondevice-actions/register";
+import "@storybook/addon-ondevice-notes/register";
+import "storybook-addon-deep-controls/register";
+
+const normalizedStories = [
+ {
+ titlePrefix: "",
+ directory: "./components",
+ files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)",
+ importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/,
+ req: require.context(
+ '../components',
+ true,
+ /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/
+ ),
+ },
+ {
+ titlePrefix: "",
+ directory: "./other_components",
+ files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)",
+ importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/,
+ req: require.context(
+ '../other_components',
+ true,
+ /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/
+ ),
+ },
+ {
+ titlePrefix: "react-native-ui",
+ directory: "../../packages/react-native-ui",
+ files: "**/!(*.factories).stories.?(ts|tsx|js|jsx)",
+ importPathMatcher: /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/,
+ req: require.context(
+ '../../../packages/react-native-ui',
+ true,
+ /^\.(?:(?:^|\/|(?:(?:(?!(?:^|\/)\.).)*?)\/)(?:(?!(?:[^/]*?\.factories))[^/]*?)\.stories\.(?:ts|tsx|js|jsx)?)$/
+ ),
+ }
+];
+
+
+declare global {
+ var view: View;
+ var STORIES: typeof normalizedStories;
+ var STORYBOOK_WEBSOCKET: { host: string; port: number } | undefined;
+ var FEATURES: Features;
+}
+
+
+const annotations = [
+ require('./preview'),
+ require("@storybook/react-native/preview"),
+ require('storybook-addon-deep-controls/preview')
+];
+
+globalThis.STORIES = normalizedStories;
+globalThis.STORYBOOK_WEBSOCKET = { host: '192.168.1.172', port: 7007 };
+
+module?.hot?.accept?.();
+
+globalThis.FEATURES.ondeviceBackgrounds = true;
+
+const options = {
+ "playFn": false
+}
+
+if (!globalThis.view) {
+ globalThis.view = start({
+ annotations,
+ storyEntries: normalizedStories,
+ options,
+ });
+} else {
+ updateView(globalThis.view, annotations, normalizedStories, options);
+}
+
+export const view: View = globalThis.view;
diff --git a/examples/expo-example/.rnstorybook/main.ts b/examples/expo-example/.rnstorybook/main.ts
index 7b80aa3ef8..0038c181a7 100644
--- a/examples/expo-example/.rnstorybook/main.ts
+++ b/examples/expo-example/.rnstorybook/main.ts
@@ -1,6 +1,6 @@
-import type { StorybookConfig } from '@storybook/react-native';
+import { defineMain } from '@storybook/react-native/node';
-const main: StorybookConfig = {
+export default defineMain({
stories: [
'../components/**/*.stories.?(ts|tsx|js|jsx)',
'../other_components/**/*.stories.?(ts|tsx|js|jsx)',
@@ -26,6 +26,4 @@ const main: StorybookConfig = {
},
framework: '@storybook/react-native',
-};
-
-export default main;
+});
diff --git a/examples/expo-example/.rnstorybook/preview.tsx b/examples/expo-example/.rnstorybook/preview.tsx
index 1bdcddad87..39e7955761 100644
--- a/examples/expo-example/.rnstorybook/preview.tsx
+++ b/examples/expo-example/.rnstorybook/preview.tsx
@@ -1,9 +1,11 @@
import { Appearance } from 'react-native';
-import type { Preview } from '@storybook/react-native';
// import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
+import { definePreview } from '@storybook/react-native';
-const preview: Preview = {
+export default definePreview({
+ addons: [],
// decorators: [withBackgrounds],
+
parameters: {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
@@ -24,14 +26,6 @@ const preview: Preview = {
my_param: 'anything',
layout: 'padded', // fullscreen, centered, padded
storybookUIVisibility: 'visible', // visible, hidden
- // backgrounds: {
- // default: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain',
- // values: [
- // { name: 'plain', value: 'white' },
- // { name: 'dark', value: '#333' },
- // { name: 'app', value: '#eeeeee' },
- // ],
- // },
backgrounds: {
options: {
// 👇 Default options
@@ -46,6 +40,4 @@ const preview: Preview = {
// 👇 Set the initial background color
backgrounds: { value: Appearance.getColorScheme() === 'dark' ? 'dark' : 'plain' },
},
-};
-
-export default preview;
+});
diff --git a/examples/expo-example/App.tsx b/examples/expo-example/App.tsx
index 9af8bc40d6..bb01807ff0 100644
--- a/examples/expo-example/App.tsx
+++ b/examples/expo-example/App.tsx
@@ -1,4 +1,9 @@
// fixes fast refresh on web
import '@expo/metro-runtime';
-export { default } from './.rnstorybook';
+const App =
+ process.env.EXPO_PUBLIC_NO_FACTORIES === 'true'
+ ? require('./.rnstorybook-nofactories').default
+ : require('./.rnstorybook').default;
+
+export default App;
diff --git a/examples/expo-example/components/ActionExample/Actions.factories.stories.tsx b/examples/expo-example/components/ActionExample/Actions.factories.stories.tsx
new file mode 100644
index 0000000000..8f303a4dd2
--- /dev/null
+++ b/examples/expo-example/components/ActionExample/Actions.factories.stories.tsx
@@ -0,0 +1,31 @@
+import { ActionButton } from './Actions';
+import { fn } from 'storybook/test';
+import preview from '../../.rnstorybook/preview';
+
+const meta = preview.meta({
+ component: ActionButton,
+ parameters: {
+ notes: `
+# Button
+
+This is a button component.
+You use it like this:
+
+\`\`\`tsx
+