This content originally appeared on DEV Community and was authored by Isaac Addis
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-adapterreact-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.cssuse@import "tailwindcss";(not v3’s@tailwind base; ...). - RN components don’t understand
classNamewithout CSS interop. Map the primitives you actually render. - Many third‑party triggers/buttons are
Pressableunder the hood — interop it specifically. - Add all RN and shared packages to
transpilePackagesor 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
transpilePackagesso Next transpiles it for the browser. - Add the library path to Tailwind
contentso utility classes used inside it are generated. - Ensure CSS interop covers the primitives the library renders (e.g.,
View,Text,TouchableOpacity, and especiallyPressablefor 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-500doesn’t- Cause: Underlying component is
Pressablewithout 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
transpilePackagesand 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.
This content originally appeared on DEV Community and was authored by Isaac Addis