Tutorial10 min read

Turn your React app into iOS and Android apps with Capacitor

A step-by-step guide to wrapping a plain React (Vite) app as native iOS and Android with Capacitor — no React Native rewrite — plus over-the-air updates with OtaKit so you can ship fixes without a store review.

The fastest way to get a React app onto the App Store and Google Play isn't React Native — it's Capacitor. Capacitor wraps your existing React build in a native shell, so the components, hooks, and libraries you already have run as-is. No new framework, no parallel codebase, no rewriting your UI in native primitives.

This guide takes a plain React app (built with Vite) to installable iOS and Android binaries, then adds OtaKit so you can push updates to the web layer over the air instead of shipping a new build for every fix.

Mental model: React Native rewrites your UI into native views. Capacitor keeps your web UI and gives it a native container. For a full breakdown of the tradeoff, see React Native vs Capacitor.

Prerequisites

  • A React app built with Vite (or any bundler that outputs static files).
  • Node 20+, Xcode for iOS builds, Android Studio for Android builds.

1. Confirm your build output

Capacitor ships static assets, so you just need a folder of built files. A Vite React app already produces one at dist/ when you run npm run build. If you're on Create React App, that folder is build/ instead — note whichever it is; Capacitor needs to know.

One thing to check: use relative asset paths. In vite.config.ts, set base: './' so assets resolve correctly from the native file:// context.

2. Add Capacitor

npm install @capacitor/core
npm install -D @capacitor/cli

npx cap init "My App" com.example.myapp --web-dir dist

npm install @capacitor/ios @capacitor/android

Your config should point at the Vite output directory:

// capacitor.config.ts
import type { CapacitorConfig } from "@capacitor/cli";

const config: CapacitorConfig = {
  appId: "com.example.myapp",
  appName: "My App",
  webDir: "dist",
};

export default config;

3. Create and run the native apps

npm run build
npx cap add ios
npx cap add android
npx cap sync

Open either platform with npx cap open ios / npx cap open android and run it. Wrap the repetitive part in a script so you never forget the sync step:

// package.json
"scripts": {
  "mobile": "vite build && npx cap sync"
}

4. Reach device features

Anything native is a Capacitor plugin you import like any React dependency. For example, the camera:

npm install @capacitor/camera
npx cap sync
import { Camera, CameraResultType } from "@capacitor/camera";

async function takePhoto() {
  const photo = await Camera.getPhoto({
    quality: 90,
    resultType: CameraResultType.Uri,
  });
  return photo.webPath;
}

On the web the same call falls back to a file input, so your components stay portable across browser and device.

5. Add over-the-air updates with OtaKit

Because your UI is the web layer, you can update it without a store round-trip. Install the plugin and configure your OtaKit app id:

npm install @otakit/capacitor-updater
npx cap sync
// capacitor.config.ts
const config: CapacitorConfig = {
  appId: "com.example.myapp",
  appName: "My App",
  webDir: "dist",
  plugins: {
    OtaKit: { appId: "YOUR_OTAKIT_APP_ID" },
  },
};

Call notifyAppReady() after your app mounts so OtaKit knows the new bundle booted successfully — if it never hears back, it rolls the device back automatically:

import { useEffect } from "react";
import { Capacitor } from "@capacitor/core";
import { OtaKit } from "@otakit/capacitor-updater";

function App() {
  useEffect(() => {
    if (Capacitor.isNativePlatform()) OtaKit.notifyAppReady();
  }, []);
  // ...
}

Install the CLI, then ship:

npm install -g @otakit/cli
otakit login

npm run build
otakit upload --release

After your first store release with the plugin in place, every otakit upload --release lands on installed devices on their next launch. Bundles are signed and verified by SHA-256, and download directly from a CDN edge — with no monthly-active-user or bandwidth metering. Native changes still require a store submission; your React bundle does not.

Want to roll out to a slice of users first? See staged rollouts for the channel-based approach.

Where to go next

The React guide has the complete setup. From there, automate releases in CI and read up on securing your update pipeline.

Related docs