It's 2025. We're in the middle of a AI-storm, tech is advancing at unprecedented speeds – yet a simple problem has been going unresolved for years. And as elementary as it sounds, it is not actually that easy to resolve: to turn a perfectly mobile-optimised website into a native mobile app.
The current reality is, that having a mobile app alongside your website requires a specialised team. And for most businesses, hiring a mobile dev team, and implementing features for Android and iPhones and keeping them in sync across all three is simply too expensive.
So why isn't it simple?
Well – the Apple and Play stores have strict rules that require apps that are featured on their stores to include native features, like notifications, or location services. It has to FEEL like a native app.
So we set that challenge to ourselves: Turn existing websites into a native mobile app
- No need to rewrite code for mobile
- Only one code base to power for all platforms
- Any new features implemented in the website are automatically implemented into the mobile app
- Looks and feels like a native app
Here's what we did
Our solution is to use React Native, an open-source JavaScript framework for building native mobile applications, to build a generic wrapper around any website. Essentially, the app uses a WebView – a mini browser inside the app – to display the live website and integrates mobile features such as navigation and push notifications on top of the WebView. This lets us ship apps for iOS and Android via the App Store and Google Play, reusing the existing code and saving huge development costs.
Why wrap? Benefits and tradeoffs
We chose a wrapper solution because it maximises code reuse and minimises cost. This approach is especially appealing if you already have a functional website or web app and a limited budget to also build an iOS or Android app.
Key benefits:
- Single codebase for web and mobile: The existing website remains the source of truth. No duplicate backend or business logic to maintain.
- Rapid time-to-market: Wrapping avoids a full native rewrite. We can go from web prototype to app store release in days or weeks, not months or years.
- Automatic updates: Any changes to the website (like fixing a bug or deploying a new feature) instantly appear in the mobile app.
- Consistent content: The app always shows the latest version of the site, so content and layout stay in sync across platforms.
Of course there are tradeoffs. Pure WebView apps may have limitations in performance (complex animations or offline support can be trickier) and some app stores frown on “just a web page” apps. To mitigate that, we intentionally add real native code – for example, page transitions, and integrating push notifications and device features. This way the stores recognise that the app isn’t a thin web shell but has genuine native functionality.
How it works
The core of the wrapper is React Native’s WebView component. A WebView acts like a mini browser inside the app without showing the entire browser UI. It can load any URL or HTML content, and supports JavaScript, cookies, and page navigation. In our case, the app simply points the WebView to the client’s website URL.
In practice, our React Native code might look like this:
import { WebView } from 'react-native-webview';
export default function HomeScreen() {
// Display the website in a full-screen WebView
return (
<WebView
source={{ uri: 'https://example.com' }}
style={{ flex: 1 }}
javaScriptEnabled={true}
/>
);
}
This simple component will open the URL https://example.com inside the app. In reality our wrapper is more elaborate: we keep a reference to the WebView to inject JavaScript if needed, we handle loading indicators, and we often disable some WebView features to match the device’s UX.
Adding native navigation
A standalone WebView is just one screen, so we build a React Navigation structure around it. For example, we might use a tab or stack navigator to switch between different site sections (like Home, Profile, Settings), or to present native screens that don’t exist on the website. On iOS vs Android we can style the navigation differently (e.g. Liquid Glass tabs on iOS, Material on Android).
A simplified example of wrapping the web app in a stack navigator:
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import { WebView } from 'react-native-webview';
const Stack = createStackNavigator();
function App() {
return (
<NavigationContainer>
<Stack.Navigator>
{/* Load the website at the root route */}
<Stack.Screen name="WebApp">
{props => (
<WebView source={{ uri: 'https://example.com' }} style={{flex:1}} {...props} />
)}
</Stack.Screen>
{/* We could add more native screens here if needed */}
</Stack.Navigator>
</NavigationContainer>
);
}
This way, navigation bars and gestures work natively. Users can hit “back” (Android) or swipe gestures and see the navigation transition, even though the content is just a webpage inside the WebView. We can also override the hardware back button on Android to walk back through WebView history. In short, the user feels like they’re in a native app with smooth navigation rather than inside a browser.
Bridging web and native code
To add deeper integration with native code, we set up a two-way message bridge between the WebView and the React Native code. The WebView API provides window.ReactNativeWebView.postMessage (from the web page) and injectJavaScript (from native) to send data back and forth. We use this to trigger app behaviour from the web, or vice versa.
For example, if the web app wants to tell the native wrapper to open a specific screen or show an alert, it can postMessage a JSON string. We listen in React Native using the WebView’s onMessage prop:
const handleWebMessage = (event) => {
const { action, payload } = JSON.parse(event.nativeEvent.data);
if (action === 'openProfile') {
navigation.navigate('ProfileScreen');
}
// handle other actions...
};
<WebView
source={{ uri: 'https://example.com' }}
onMessage={handleWebMessage}
/>
Then, to send the event on the web side, just call
window.ReactNativeWebView.postMessage(JSON.stringify({action: 'openProfile'}));
In order to parse this event in React Native we do:
const { action, payload } = JSON.parse(event.nativeEvent.data);
and handle the action accordingly. This keeps communications decoupled and type-safe, so the web and native sides don’t have to know each other’s internals (they just agree on message format). This bridge allows the website to request native capabilities – for instance, the web code might ask the app to take a photo using the device camera, or to share content via the native share sheet.
Enabling push notifications
Push notifications are a key native feature that most of our clients want and so we integrated a push service (e.g. Firebase Cloud Messaging for Android, APNs for iOS) into the React Native wrapper. At app launch we ask the user for permission and retrieve a device push token. For example, using react-native-firebase, our code looks like:
import messaging from '@react-native-firebase/messaging';
useEffect(() => {
async function initFCM() {
await messaging().requestPermission();
const token = await messaging().getToken();
// Send token to our backend so it can push messages to this device
console.log('Push token:', token);
}
initFCM();
// Handle incoming message (app in foreground)
const unsubscribe = messaging().onMessage(async message => {
Alert.alert('New Notification', message.notification.title);
});
return unsubscribe;
}, []);
This ensures the app can receive push messages even if the website itself has no push logic. Many web apps rely on service workers for notifications in a browser but those don’t fire inside a mobile WebView so the wrapper acts as a proxy. We relay notification events into the web content if needed by using our message bridge. By enabling native push notifications we can wire the events into the user’s experience (e.g. tapping a notification might open a specific page within the WebView).
Building & distributing the app
Once the web app is wrapped in our native app, packaging it for the App Store and Google Play is straightforward. We set up two separate native projects (iOS and Android) using the same React Native code. Expo Application Services (EAS) streamlines the process from build to distribution. The important part is that no changes are needed on the web app.
Because we included real native features (not just a cookie-cutter webview), approvals go smoothly. In short, from a technical standpoint the app’s build and submission steps are identical to any other React Native app, except the app’s content is our client’s website rather than custom JSX.
When to use this pattern
Wrapping a website in a native shell is not always the right choice. It works best when the site is already mobile first, responsive and well-tested on mobile browsers. It’s ideal for content-driven apps (news, blogs, directories, dashboards) where the bulk of the logic lives on the server or the existing frontend. It can work for interactive apps too, but highly complex UIs or performance-critical features (like 3D graphics, heavy animations) may not run smoothly in a WebView. Below are a few use cases we think are good candidates for this approach:
- Startups or SMBs with an existing PWA/website that want quick mobile presence without doubling dev teams.
- Limited budgets: A single web team can maintain features and updates, and both web and mobile users benefit.
- Rapid MVPs: Testing market demand on mobile without going all in.
It’s also important to consider the limitations for this approach:
- Offline: Since the WebView typically loads online content, offline access is limited unless you implement extra caching.
- Performance: Modern devices handle WebViews well, but very heavy JS/DOM may still lag compared to native components.
- Store rules: Both Apple and Google ask that “wrapper” apps provide value beyond just a website. Including push, native navigation, any device integration (camera, location, etc.) helps meet those guidelines.
In summary
Our React Native wrapper is able to turn any website into a cross-platform mobile app, complete with native navigation and notifications, without touching the site’s code and the need to maintain multiple code bases. The key steps are: embed the site URL in a WebView, add native navigation, implement a message bridge for two-way communication, and integrate push notifications. The result is a publishable and fully functional app that uses your website code to power both web and mobile platforms.
