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)