Setting Up Universal Storybook (with NativeWind): A Step-by-Step Guide

Meenu MakkarMeenu Makkar
6 min read

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?

  1. react-native Storybook on web is not feature rich, so we will run storybook-react on web and storybook/react-native on mobile.

  2. 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

  1. 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.

  2. 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 script reset-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.

  3. 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.

  4. 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 .

  5. 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

  1. Move out stories directory from .storybook directory to the root of your project.

     mv .storybook/stories stories
    
  2. 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",
  1. Run the Storybook again to check if the changes done are correct or not.

     npm run start
    
  2. Rename .storybook to .ondevice : We have to setup different configuration files for storybook-web.

     mv .storybook .ondevice
    
  3. 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.

  4. Run the storybook again to check if the changes done are working or not.

  5. create a new .storybook folder for web storybook configuration.

  6. Create a preview.tsx and main.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;
    
  7. 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
    
  8. 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

  1. Create an app.config.ts file if not there already and add this to your file.

     extra: {
         storybookEnabled: process.env.STORYBOOK_ENABLED,
       },
    
  2. 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;
    
  3. 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

  1. Setup NativeWind: Follow the instructions specified in the NativeWind documentation https://www.nativewind.dev/v4/getting-started/expo-router

  2. Add dev-dependencies:

     npm i -D autoprefixer postcss-loader
    
  3. Create postcss.config.js file in the root directory of project.

     // postcss.config.js file
    
     module.exports = {
         plugins: {
             tailwindcss: {},
             autoprefixer: {},
         },
     };
    
  4. Add add-on in the main.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") },
               },
             ],
           },
         ],
       },
     },
     ]
    
  5. Add loaders for NativeWind in webpackFinal in main.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;
       },
    
  6. Import global.css file in the app/_layout.tsx file, .storybook/preview.tsx file, .ondevice/preview.tsx file, app/index.tsx file, and story file.

     // _layout.tsx file/
     // .storybook/preview.tsx
     // .ondevice/preview.tsx
     // app/index.tsx
     // Button.tsx
    
     import "../global.css"
    
  7. 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}",]
    
  8. 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

  1. mdx file support:

Error: Unexpected JSX child: JSXFragment

You cannot write mdx file with .stories.mdx extension. Correct way: Button.mdx .

0
Subscribe to my newsletter

Read articles from Meenu Makkar directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Meenu Makkar
Meenu Makkar

I am a frontend developer and a full time open source contributor.