Running on the Web
How react-native-web runs your app in a browser through Expo, what it cannot do, and when web is a real target rather than a demo.
npx expo start --web | npx expo export --platform webReact Native for Web implements React Native's components with DOM elements: View becomes a div, Text renders text, and StyleSheet styles compile to CSS classes. Expo wires it in, so npx expo start --web serves the same source your phone runs, bundled by Metro. Add the dependencies with npx expo install react-dom react-native-web @expo/metro-runtime; the last of those provides the runtime that loads the chunks Expo CLI splits web bundles into for production.
Output modes
web.output in the app config decides what npx expo export --platform web produces. single, the default, writes one index.html: a client-rendered single-page app with no HTML for crawlers. static writes an HTML file per route, which is what you need if the site should be indexed. server writes client and server directories for a Node host, which is what API routes require. Metro is the bundler; the old @expo/webpack-config is deprecated.
What does not work
Anything backed by native code. Maps, push notifications, in-app purchases and most sensors have no browser implementation, and the honest answer is per module: each one's documentation page carries a platform table. Styling is the quieter problem, because it looks like CSS and is not - shadows are the camelCase boxShadow prop, added in React Native 0.76 and available on the New Architecture, or the older shadow* and elevation props, never a kebab-case CSS box-shadow declaration, and nothing inherits to children outside Text.
Is web a real target
If it is, you owe it responsive layouts, hover and keyboard states, real URLs and server-rendered HTML. Platform.OS === "web" ? a : b and Platform.select({ web: a, default: b }) are the same check. If web is only a demo for reviewers, say so and stay on the single-page default.
Example
// src/app/index.tsx - one file, two shapes: a column on a phone, a row on a wide window
import {
Platform,
StyleSheet,
Text,
useWindowDimensions,
View,
} from "react-native";
type PanelProps = { title: string; body: string };
function Panel({ title, body }: PanelProps) {
return (
<View style={styles.panel}>
<Text style={styles.title}>{title}</Text>
<Text>{body}</Text>
</View>
);
}
export default function HomeScreen() {
// Re-renders when a browser window is resized, not just on rotation
const { width } = useWindowDimensions();
const wide = width >= 900;
return (
<View style={[styles.screen, wide && styles.screenWide]}>
<Panel title="Orders" body="Everything placed today" />
<Panel title="Stock" body="Items running low" />
<Text style={styles.footer}>
{Platform.select({ web: "Running in a browser", default: "Running natively" })}
</Text>
</View>
);
}
const styles = StyleSheet.create({
// flexDirection defaults to column here, unlike CSS. No vh units, no media queries.
screen: { flex: 1, padding: 16, gap: 16 },
screenWide: { flexDirection: "row", flexWrap: "wrap" },
panel: { flex: 1, padding: 16, borderRadius: 12, backgroundColor: "#f1f5f9" },
title: { fontSize: 18, fontWeight: "600", marginBottom: 4 },
footer: { textAlign: "center" },
});When to use it
- Giving a client a link to click instead of installing a build on their device.
- Shipping a marketing or docs site from the same codebase as the app.
- Debugging layout in browser devtools while building a screen.
- Deciding, before you promise it, whether a feature can exist in the browser at all.
More examples
Choosing the web output
output: static gives each route its own HTML file, so a crawler and a cold first paint both get real markup - the setting to change the moment the web build is meant for strangers rather than for your team. Leave it at the default single for an internal demo, and use server only when you are hosting API routes on Node.
{
"expo": {
"name": "Orders",
"slug": "orders",
"web": {
"bundler": "metro",
"output": "static",
"favicon": "./assets/favicon.png"
}
}
}A platform-split component
Callers write import VenueMap from "@/components/VenueMap" and Metro resolves the .web.tsx file when bundling for web, so react-native-maps never reaches the browser build. The type-only import carries the props across without pulling in any code, because type imports are erased at build time. On native you see an interactive map with a pin; on web, a grey panel that opens a map site.
// src/components/VenueMap.tsx - the native file
import { StyleSheet } from "react-native";
import MapView, { Marker } from "react-native-maps";
export type VenueMapProps = { latitude: number; longitude: number };
export default function VenueMap({ latitude, longitude }: VenueMapProps) {
const region = { latitude, longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 };
return (
<MapView style={styles.map} initialRegion={region}>
<Marker coordinate={{ latitude, longitude }} />
</MapView>
);
}
const styles = StyleSheet.create({
map: { height: 240, borderRadius: 12 },
});
// src/components/VenueMap.web.tsx - Metro picks this one for the web bundle
import { Linking, Pressable, StyleSheet, Text } from "react-native";
import type { VenueMapProps } from "./VenueMap";
export default function VenueMap({ latitude, longitude }: VenueMapProps) {
const url = `https://www.openstreetmap.org/?mlat=${latitude}&mlon=${longitude}`;
return (
<Pressable style={styles.fallback} onPress={() => Linking.openURL(url)}>
<Text>Open this location in a map</Text>
</Pressable>
);
}
const styles = StyleSheet.create({
fallback: { height: 240, borderRadius: 12, justifyContent: "center", alignItems: "center", backgroundColor: "#e2e8f0" },
});Build and serve the site
The export writes plain static files, so any static host or CDN can serve them - except with output set to server, which also emits a server directory that needs a Node process. Serving dist locally is worth doing every time, because static rendering runs your code without a native runtime and that is where web-only crashes surface.
# install the web dependencies into an existing project
npx expo install react-dom react-native-web @expo/metro-runtime
# develop against a browser
npx expo start --web
# produce a deployable site in dist/
npx expo export --platform web
# check the built output locally before uploading it anywhere
npx serve dist
Discussion