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
- Install the library and its peer deps (example uses
@isaacaddis/private-rn-library
). - Add the library to
transpilePackages
so Next transpiles it for the browser. - Add the library path to Tailwind
content
so utility classes used inside it are generated. - Ensure CSS interop covers the primitives the library renders (e.g.,
View
,Text
,TouchableOpacity
, and especiallyPressable
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>
);
}
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;
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"]
}
.babelrc (Babel config)
{
"presets": ["next/babel", "@babel/preset-env", "@babel/preset-flow"],
"plugins": [
[
"@babel/plugin-transform-react-jsx",
{
"runtime": "automatic",
"importSource": "nativewind"
}
]
]
}
nativewind-env.d.ts
/// <reference types="nativewind/types" />
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: [],
};
postcss.config.mjs (Tailwind v4)
const config = {
plugins: ["@tailwindcss/postcss"],
};
export default config;
styles/globals.css (Tailwind v4 import)
@import "tailwindcss";
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} />;
}
Issues and fixes
-
No background colors rendering
- Cause: Tailwind v3 directives used with Tailwind v4 → CSS never generated.
- Fix: Use
@import "tailwindcss";
inglobals.css
.
-
Border width shows, but
border-red-500
doesn’t- Cause: Underlying component is
Pressable
without CSS interop. - Fix:
cssInterop(Pressable, { className: "style" })
.
- Cause: Underlying component is
-
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.
- Fix: Add
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)