Setting Up Universal Storybook (with NativeWind): A Step-by-Step Guide
Develop UI components in isolation with consistent design in React Native apps! Set up a Universal Storybook with NativeWind in an Expo project.
Table of contents
In this tutorial, we will walk through the process of setting up a Universal Storybook with NativeWind in an Expo project.
Storybook is a powerful tool for developing UI components in isolation, and NativeWind provides a set of utility classes for building consistent UI designs in React Native applications.
How Is It Different from Standard Setup?
react-native Storybook on web is not feature rich, so we will run storybook-react on web and storybook/react-native on mobile.
Setting up NativeWind on Storybook is not straight forward. We need to add some loaders and imports to pick up styles.
A Look at the Versions Used
Expo: SDK 51
React Native: 0.74.1
Storybook: [version] : react-native: 7.6.18 (latest) web: 8.1.0 (latest)
NativeWind: 4.0.36 (alpha)
Tailwind version: 3.4.3 (latest)
Setting the Prerequisites
Before we begin, make sure you have the following installed:
Node.js (>18) and npm (>9)
expo-cli
storybook-cli
Step 1: Setup React Native Storybook
Boot an expo project: Run this command where you want to make a project in your computer:
npx create-expo-app storybook-nativewind
I named my project as
storybook-nativewind
. Follow the prompts to set up your project.Reset your project app directory: Go to the project directory and run this command:
npm run reset-project
In the latest booted project version of
create-expo-app
, we have a scriptreset-project
, which makes the project a fresh project. This will move the current app directory to app-example and just keep the fresh layout and index file.Install Storybook: Run this command to initiate Storybook setup:
npx storybook@latest init
Follow the on-screen instructions to complete the setup. It will add a basic story and storybook config to your expo project. It will also install all the necessary dependencies needed.
Run Storybook: Setup main entry file of your expo app to run Storybook.
// App.tsx or app/index.tsx or _layout.tsx // import { Text, View } from "react-native"; // export default function Index() { // return ( // <View // style={{ // flex: 1, // justifyContent: "center", // alignItems: "center", // }} // > // <Text>Edit app/index.tsx to edit this screen.</Text> // </View> // ); // } export { default } from '../.storybook';
Adjust export path according to the relative path of
main file
and.storybook folder
.Setup metro config: Create metro config file if you do not have it yet. Run this on root of the project:
npx expo customize metro.config.js
Then set
transformer.unstable_allowRequireContext
to true and add the generate call here.//metro.config.js const path = require('path'); const { getDefaultConfig } = require('expo/metro-config'); const { generate } = require('@storybook/react-native/scripts/generate'); generate({ configPath: path.resolve(__dirname, './.storybook'), }); /** @type {import('expo/metro-config').MetroConfig} */ const config = getDefaultConfig(__dirname); config.transformer.unstable_allowRequireContext = true; config.resolver.sourceExts.push('mjs'); module.exports = config;
And your Storybook is set! Now run
npm run start
to run your storybook on any device.
Step 2: Setup Web Storybook
Move out
stories
directory from.storybook
directory to the root of your project.mv .storybook/stories stories
Since you have changed the path of
stories
, you need to fix this in the configuration files..storybook/main.ts
file
- stories: ["./stories/**/*.stories.?(ts|tsx|js|jsx)"]
+ stories: ["../stories/**/*.stories.?(ts|tsx|js|jsx)"]
.storybook/storybook.require.ts
file
- directory: ".storybook/stories"
+ directory: "./stories"
- req: require.context(
"./stories",
+ req: require.context(
"../stories",
Run the Storybook again to check if the changes done are correct or not.
npm run start
Rename
.storybook
to.ondevice
: We have to setup different configuration files for storybook-web.mv .storybook .ondevice
Fix configuration paths after change the name of folder to
.storybook
to.ondevice
.a. Change the path in
metro.config.js
file.b. Change the path in app entry file.
Run the storybook again to check if the changes done are working or not.
create a new
.storybook
folder for web storybook configuration.Create a
preview.tsx
andmain.ts
file:// preview.tsx // copy the content from .ondevice preview.tsx import type { Preview } from "@storybook/react"; const preview: Preview = { parameters: { controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, }, }; export default preview;
// main.ts const main = { stories: [ "../stories/**/*.stories.mdx", "../stories/**/*.stories.?(ts|tsx|js|jsx)", ], addons: [ "@storybook/addon-links", "@storybook/addon-essentials", "@storybook/addon-react-native-web", ], framework: { name: "@storybook/react-webpack5", options: {}, }, docs: { autodocs: true, }, }; export default main;
Install the Storybook dev-dependencies for storybook-web.
npm i -D @storybook/addon-actions@latest @storybook/addon-controls@latest @storybook/addon-essentials@latest @storybook/addon-links@latest @storybook/addon-ondevice-backgrounds@latest @storybook/addon-ondevice-notes@latest @storybook/addon-react-native-web@latest @storybook/addon-styling-webpack@latest @storybook/react@latest @storybook/react-webpack5@latest storybook@latest
Run storybook-web: Add scripts in your package.json file to run the Storybook.
// package.json "storybook:web": "storybook dev -p 6006", "build-storybook": "storybook build", // for stories generation for native "storybook-generate": "sb-rn-get-stories --config-path .ondevice",
// run the storybook web npm run storybook:web // run the storybook native npm run start and then choose ios or android
Step 3: Run Storybook and App Simultaneously with Environment Variable Configuration
Create an
app.config.ts
file if not there already and add this to your file.extra: { storybookEnabled: process.env.STORYBOOK_ENABLED, },
Go to your main entry file of your application and add the conditionals to run Storybook or app.
// App.tsx or app/index.tsx // to import from extra field of app.config.ts import Constants from "expo-constants"; function App() { return ( <View> <Text>Open up App.tsx to start working on your app!</Text> </View> ); } let AppEntryPoint = App; if (Constants.expoConfig?.extra?.storybookEnabled === "true") { AppEntryPoint = require("./.ondevice").default; } export default AppEntryPoint;
Update scripts in the package.json to run Storybook and app.
"start": "expo start", "android": "expo start --android", "ios": "expo start --ios", "web": "expo start --web", "storybook:web": "storybook dev -p 6006", "build-storybook": "storybook build", "storybook-generate": "sb-rn-get-stories --config-path .ondevice", "storybook": "cross-env STORYBOOK_ENABLED='true' expo start", "storybook:ios": "cross-env STORYBOOK_ENABLED='true' expo start --ios", "storybook:android": "cross-env STORYBOOK_ENABLED='true' expo start --android"
Install
cross-env
as a dev dependency if not existing already.
Step 4: Run NativeWind in Storybook
Setup NativeWind: Follow the instructions specified in the NativeWind documentation https://www.nativewind.dev/v4/getting-started/expo-router
Add dev-dependencies:
npm i -D autoprefixer postcss-loader
Create
postcss.config.js
file in the root directory of project.// postcss.config.js file module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, };
Add
add-on
in themain.ts
file of.storybook
(for web).//main.ts file // other configuration addons: [ ... { name: "@storybook/addon-styling-webpack", options: { rules: [ // Replaces existing CSS rules to support PostCSS { test: /\.css$/, use: [ "style-loader", { loader: "css-loader", options: { importLoaders: 1 }, }, { // Gets options from `postcss.config.js` in your project root loader: "postcss-loader", options: { implementation: require.resolve("postcss") }, }, ], }, ], }, }, ]
Add loaders for NativeWind in
webpackFinal
inmain.ts
file of.storybook
(for web).//main.ts file //other configuration webpackFinal: async (config: any) => { config.module.rules.push({ test: /\.(js|jsx|ts|tsx)$/, loader: "babel-loader", options: { presets: [ ["babel-preset-expo", { jsxImportSource: "nativewind" }], "nativewind/babel", ], plugins: ["react-native-reanimated/plugin"], }, }); return config; },
Import
global.css
file in theapp/_layout.tsx
file,.storybook/preview.tsx
file, .ondevice/preview.tsx
file,app/index.tsx
file, andstory
file.// _layout.tsx file/ // .storybook/preview.tsx // .ondevice/preview.tsx // app/index.tsx // Button.tsx import "../global.css"
Add paths of stories in your
tailwind.config.ts
file.// tailwind.config.js file content: ["./app/**/*.{js,jsx,ts,tsx}", "./stories/**/*.{js,jsx,ts,tsx}",]
Testing NativeWind functionality: Add classNames in your components in stories files.
<Text className="bg-blue-500"> Hello Nativewind world! </Text>
Summing Up
You have successfully setup your Storybook on Expo Web, Expo Native, React Web with NativeWind setup!
If you get stuck while working through this guide, refer to the full example on GitHub.
A Common Issue You Might Encounter
- mdx file support:
Error: Unexpected JSX child: JSXFragment
You cannot write mdx file with .stories.mdx
extension. Correct way: Button.mdx
.