react-native-web
React Native for Web implements much of React Native's component and JavaScript API on top of React DOM. It lets a shared component import View, Text, Pressable, StyleSheet, Platform, and related APIs from react-native while a web bundler redirects those imports to react-native-web. It is an adaptation layer, not a mobile runtime in a browser: DOM output, browser accessibility, CSS behavior, and unsupported native APIs still require web-specific judgment.
The established bridge for teams with real React Native code to share, especially through Expo. Do not install it merely to make a web-only app look cross-platform; the aliasing, type package, API gaps, and native-module boundaries are real ongoing costs.
Use it if
- You have a React Native application and want to share presentation and interaction code with a browser target
- You are building through Expo, which includes web integration and the package's recommended configuration path
- Your design system can stay within the documented cross-platform component and style subset
- You need React Native-style Pressable, accessibility props, and atomic StyleSheet output in an existing React web application
- Your product is web-only and needs ordinary semantic HTML, forms, CSS selectors, and browser libraries throughout; React DOM is the more direct abstraction
- You require full React Native API parity: the compatibility table marks Alert, Settings, TouchableNativeFeedback, and several other exports unsupported, and Animated has no useNativeDriver support
- You expect TypeScript declarations in the runtime package: 0.21.2 ships Flow source but no types field, and the official guide sends TypeScript users to the separately maintained @types/react-native-web package
- You cannot own bundler, test-runner, and server-rendering aliases; the setup guide requires react-native to resolve to react-native-web in each environment that evaluates shared imports
- Your app depends heavily on third-party native modules: NativeModules is mocked on web, and only packages with an explicit web implementation can provide native functionality
Setup reality
The shortest install is npm install react-dom react-native-web, but a shared-code setup has more edges. Version 0.21.2 requires React and React DOM 18 or 19 as peers. Configure your bundler so the exact react-native import resolves to react-native-web; repeat that mapping in Jest, Babel, and any Node process used for pre-rendering, or the same file can pass in the browser build and fail in tests or SSR. The docs recommend babel-plugin-react-native-web for per-export rewriting and dead-code removal. TypeScript declarations are not bundled, so install @types/react-native-web and add react-native-web to compilerOptions.types when augmenting React Native's props and style types. Full-screen ScrollView shells need explicit html, body, and root height rules, often with body overflow hidden. Browser support targets recent engines, but the guide says to supply Promise, Object.assign, Array.from, and ResizeObserver polyfills when supporting older environments. Native modules do not become browser APIs: use Platform checks or .web.js files for meaningful differences, verify every third-party React Native dependency has web support, and keep server-only evaluation away from document and window. Expo removes much of this wiring and is the project's recommended multi-platform starting point.
Patterns
Register and run a web applicationrender-application
import { AppRegistry, Text, View } from 'react-native';
function App() {
return <View><Text>Hello, web</Text></View>;
}
AppRegistry.registerComponent('App', () => App);
AppRegistry.runApplication('App', {
rootTag: document.getElementById('root'),
});Shared files import from react-native; your web build must alias that exact package name to react-native-web.
Alias React Native in webpackconfigure-webpack-alias
// webpack.config.js
module.exports = {
resolve: {
alias: {
'react-native$': 'react-native-web',
},
extensions: ['.web.js', '.js', '.json'],
},
};The trailing dollar makes the alias exact. Put .web.js before .js when shared imports should select web-specific files.
Apply the same alias in Jestconfigure-jest-alias
// jest.config.js
module.exports = {
moduleNameMapper: {
'^react-native$': 'react-native-web',
},
};Bundler aliases do not automatically affect Jest; without this mapping, shared tests may load React Native's native entry.
Enable per-export Babel transformsoptimize-babel-imports
// babel.config.json
{
"plugins": ["react-native-web"]
}Install babel-plugin-react-native-web separately. The project recommends it for pruning unused modules at build time.
Create reusable stylescreate-responsive-styles
import { StyleSheet, Text, View } from 'react-native';
const styles = StyleSheet.create({
card: { padding: 16, borderRadius: 8, backgroundColor: '#fff' },
title: { fontSize: 18, fontWeight: '600' },
});
export function Card() {
return <View style={styles.card}><Text style={styles.title}>Title</Text></View>;
}React Native style objects use camelCase and a supported subset of CSS; descendant selectors and ordinary CSS cascade rules are not part of StyleSheet.create.
Style a Pressable from interaction statehandle-press-state
import { Pressable, Text } from 'react-native';
<Pressable
accessibilityRole="button"
onPress={() => save()}
style={({ hovered, focused, pressed }) => ({
opacity: pressed ? 0.6 : 1,
outlineStyle: focused ? 'solid' : 'none',
backgroundColor: hovered ? '#eee' : '#fff',
})}
>
<Text>Save</Text>
</Pressable>hovered and focused are web-relevant states. Keep keyboard focus visible rather than removing outlines without a replacement.
Use Platform for a small web differencebranch-small-differences
import { Platform, StyleSheet } from 'react-native';
const styles = StyleSheet.create({
panel: {
height: Platform.OS === 'web' ? 240 : 180,
cursor: Platform.OS === 'web' ? 'pointer' : undefined,
},
});Use Platform for small branches only. Separate .web.js and .native.js files are easier to maintain when markup or behavior diverges.
Provide a web-specific component implementationsplit-platform-files
// ShareButton.web.js
export function ShareButton({ url }) {
return <button onClick={() => navigator.clipboard.writeText(url)}>Copy link</button>;
}
// ShareButton.native.js
import { Share } from 'react-native';
export function ShareButton({ url }) {
return <Button title="Share" onPress={() => Share.share({ message: url })} />;
}Configure web resolution to prefer .web.js. Native modules and browser APIs should stay in their platform files.
Prepare a full-height ScrollView shellmake-full-height-root
/* Inline or load before the app mounts */
html, body { height: 100%; }
body { overflow: hidden; }
#root { display: flex; height: 100%; }The setup guide recommends this shell for full-screen apps with a root ScrollView; body overflow hidden is wrong for ordinary document-scrolling pages.
Get an application element for server renderingrender-server-html
import { renderToString } from 'react-dom/server';
import { AppRegistry } from 'react-native-web';
import App from './App.js';
AppRegistry.registerComponent('App', () => App);
const { element, getStyleElement } = AppRegistry.getApplication('App');
const html = renderToString(element);
const css = renderToString(getStyleElement());Server resolution must point react-native imports at react-native-web too. Keep modules that touch window or document out of the server import path.
Augment React Native types for web propsadd-typescript-support
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"types": ["react-native-web"]
}
}
// install separately: npm install -D @types/react-native-webThe runtime package has no bundled TypeScript declarations. Its official TypeScript guide relies on DefinitelyTyped.
Respond to viewport changesadapt-window-size
import { Text, useWindowDimensions, View } from 'react-native';
export function Layout() {
const { width } = useWindowDimensions();
const columns = width >= 900 ? 3 : width >= 600 ? 2 : 1;
return <View style={{ flexDirection: 'row' }}><Text>{columns} columns</Text></View>;
}The hook updates when dimensions change. Avoid reading window directly in shared or server-rendered components.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| react-dom | npm | You are building a web-first React application and want direct DOM semantics and browser ecosystem access |
| expo | npm | You want a managed universal app stack that configures React Native for Web and adds cross-platform APIs |
| react-strict-dom | npm | You want Meta's experimental web-first component layer with stricter cross-platform styling rules |
| @tamagui/core | npm | You want a cross-platform styled-component system and compiler rather than broad React Native API emulation |