DEV Community

Isaac Addis
Isaac Addis

Posted on • Originally published at isaacaddis.github.io

Using React Native Components in a Next.js Web App (via @expo/next-adapter)

Using React Native Components in a Next.js Web App (via @expo/next-adapter)

This post documents how I imported a React Native component library into a Next.js pages‑router app and rendered it on the web using React Native Web, @expo/next-adapter, and Babel. This configuration is a viable option for preventing duplicate code across a mobile + NextJS app codebase.

Tech Stack

  • Next.js 15 (pages router) + React 19
  • React Native Web
  • Tailwind CSS v4 + NativeWind v4
  • @expo/next-adapter
  • react-native-css-interop
  • Babel via .babelrc (Next disables SWC when a custom Babel config is present)
  • Shared RN components (e.g., @isaacaddis/private-rn-library), RN primitives (@rn-primitives/*)

Key ideas

  • Tailwind v4 uses a new import model: in globals.css use @import "tailwindcss"; (not v3’s @tailwind base; ...).
  • RN components don’t understand className without CSS interop. Map the primitives you actually render.
  • Many third‑party triggers/buttons are Pressable under the hood — interop it specifically.
  • Add all RN and shared packages to transpilePackages or you’ll hit syntax/runtime errors.
  • Set important: "html" so Tailwind wins over other stylesheets.
  • NativeWind works in the pages router and "use client" routes; RSC support is in progress.

Integrating the React Native library

  1. Install the library and its peer deps (example uses @isaacaddis/private-rn-library).
  2. Add the library to transpilePackages so Next transpiles it for the browser.
  3. Add the library path to Tailwind content so utility classes used inside it are generated.
  4. Ensure CSS interop covers the primitives the library renders (e.g., View, Text, TouchableOpacity, and especially Pressable for triggers/buttons).

After these steps, components can be imported and used as follows:

import { Card, Dialog, DialogTrigger } from "@isaacaddis/private-rn-library";
import { Text } from "react-native";

export default function Page() {
  return (
    <div className="p-6">
      <Card className="rounded-xl border p-4">
        <Text>Card content</Text>
      </Card>
      <Dialog>
        <DialogTrigger className="bg-blue-600 p-3 rounded">
          <Text className="text-white">Open</Text>
        </DialogTrigger>
      </Dialog>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

next.config.ts

import { withExpo } from "@expo/next-adapter";

/** @type {import('next').NextConfig} */
const nextConfig = withExpo({
  reactStrictMode: true,
  transpilePackages: [
    "react-native",
    "react-native-web",
    "nativewind",
    "react-native-css-interop",
    "@rn-primitives",
    "@isaacaddis/private-rn-library",
    "react-native-reanimated",
  ],
  webpack: (config) => {
    config.resolve.alias = {
      ...(config.resolve.alias || {}),
      "react-native$": "react-native-web",
      "phosphor-react-native": "phosphor-react",
    };
    return config;
  },
});
export default nextConfig;
Enter fullscreen mode Exit fullscreen mode

tsconfig.json (please note the jsxImportSource line)

{
  "compilerOptions": {
    "jsxImportSource": "nativewind",
    "jsx": "preserve",
    "moduleResolution": "bundler",
    "strict": true
  },
  "include": ["next-env.d.ts", "nativewind-env.d.ts", "**/*.ts", "**/*.tsx"]
}
Enter fullscreen mode Exit fullscreen mode

.babelrc (Babel config)

{
  "presets": ["next/babel", "@babel/preset-env", "@babel/preset-flow"],
  "plugins": [
    [
      "@babel/plugin-transform-react-jsx",
      {
        "runtime": "automatic",
        "importSource": "nativewind"
      }
    ]
  ]
}
Enter fullscreen mode Exit fullscreen mode

nativewind-env.d.ts

/// <reference types="nativewind/types" />
Enter fullscreen mode Exit fullscreen mode

tailwind.config.js (Tailwind v4 + NativeWind)

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./pages/**/*.{ts,tsx,js,jsx}",
    "./components/**/*.{ts,tsx,js,jsx}",
    "./node_modules/@isaacaddis/private-rn-library/**/*.{ts,tsx,js,jsx}",
  ],
  presets: [require("nativewind/preset")],
  important: "html",
  theme: {
    extend: {
      // tokens (colors, sizes, etc.)
    },
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

postcss.config.mjs (Tailwind v4)

const config = {
  plugins: ["@tailwindcss/postcss"],
};
export default config;
Enter fullscreen mode Exit fullscreen mode

styles/globals.css (Tailwind v4 import)

@import "tailwindcss";
Enter fullscreen mode Exit fullscreen mode

pages/_app.tsx (CSS interop for RN primitives)

import "@/styles/globals.css";
import type { AppProps } from "next/app";
import { cssInterop } from "react-native-css-interop";
import { View, Text, TouchableOpacity, Pressable } from "react-native";

// Map className -> style for primitives actually rendered in your app/libs
cssInterop(View, { className: "style" });
cssInterop(Text, { className: "style" });
cssInterop(TouchableOpacity, { className: "style" });
cssInterop(Pressable, { className: "style" }); // critical for many Trigger components

export default function App({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />;
}
Enter fullscreen mode Exit fullscreen mode

Issues and fixes

  • No background colors rendering

    • Cause: Tailwind v3 directives used with Tailwind v4 → CSS never generated.
    • Fix: Use @import "tailwindcss"; in globals.css.
  • Border width shows, but border-red-500 doesn’t

    • Cause: Underlying component is Pressable without CSS interop.
    • Fix: cssInterop(Pressable, { className: "style" }).
  • SyntaxError / Unexpected tokens from node_modules

    • Cause: Untranspiled RN/shared packages.
    • Fix: Add them to transpilePackages and ensure the library ships browser‑compatible JS.
  • Styles present but overridden

    • Fix: Add important: "html" to Tailwind config to increase specificity.

Conclusion

Using @expo/next-adapter with React Native Web, Tailwind v4, NativeWind, react-native-css-interop, and Babel allows for importing a React Native library inside a Next.js web app without duplicating UI code. The required steps are: transpile React Native and the library, use Tailwind v4’s @import CSS, include the library paths in Tailwind content, and map React Native primitives (including Pressable) with CSS interop so className resolves to styles.

Top comments (0)