EmpireUI
Get Pro
← Blog9 min read#capacitor#mobile#react

Capacitor + React: Mobile Apps From Your Web Codebase

Ship iOS and Android apps from your existing React codebase using Capacitor. Full setup, native plugins, and real gotchas from production builds.

smartphone screen showing React app running natively on mobile device

Why Capacitor Over React Native

Look, if you've already got a React app running in the browser, rewriting it in React Native is a painful choice. Two codebases, two build pipelines, and a completely different component model. Capacitor takes a different approach: wrap your existing web app in a native shell and give it access to device APIs through a plugin layer. That's the whole pitch.

It's not magic. Your app still runs in a WebView, so there's no pretending it performs like a fully native app. But for a huge category of apps — dashboards, productivity tools, internal tools, content-heavy apps — it's genuinely good enough. Honestly, the performance gap between a well-optimized WebView app and a native one has shrunk dramatically since 2023, especially on iOS 17+ with the Safari engine improvements.

Where Capacitor really wins is if your team is strong in React and CSS but has zero Swift or Kotlin experience. You get push notifications, camera access, file system, biometrics — all of it — without touching native code. Worth noting: Capacitor 6 (released in 2024) aligned much more closely with the native build toolchains, so the Xcode and Android Studio integration feels less janky than it used to.

That said, if your app is a game, a video editor, or anything that needs 60fps animations at the native layer, Capacitor isn't the right tool. Use React Native or go full native.

Project Setup: From Zero to Running

Start with an existing React or Vite project. Capacitor doesn't care how you built your web app — it just needs a dist folder and an index.html at the root of it. If you're using Next.js with output: 'export', that works too, but you lose server-side features entirely. For most Capacitor use cases, Vite + React is the cleanest combo.

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install
npm install @capacitor/core @capacitor/cli
npm install @capacitor/ios @capacitor/android

Then initialize Capacitor with your app name and ID. The app ID follows reverse-domain convention — use something real here because changing it later requires re-signing your app in both stores.

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

Build your web app first, then add the native platforms. Capacitor copies your dist folder into the native project — it doesn't hot-reload from a dev server by default, though you can set that up separately.

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

After cap sync, open the native projects: npx cap open ios launches Xcode, npx cap open android launches Android Studio. From there you build and run on a simulator or device exactly like you would any native app. Quick aside: you'll need a Mac with Xcode 15+ to build for iOS — there's no way around that requirement.

The Dev Loop: Live Reload Without Pain

The default build-sync-open loop is fine for occasional checks, but you don't want to run npm run build && npx cap sync after every CSS tweak. Capacitor has a live reload mode that points the native WebView at your local dev server instead of the bundled files.

npx cap run ios --livereload --external
npx cap run android --livereload --external

The --external flag makes the dev server bind to your machine's LAN IP rather than localhost, so the device or simulator can actually reach it. Your phone needs to be on the same network. If you're on a corporate network with AP isolation, this won't work — use a simulator instead.

One more thing — live reload re-uses whatever vite dev server you have running. Hot module replacement works as you'd expect: change a component, see it update on the device within a second or two. State is preserved across most HMR updates, just like in the browser.

In practice, the workflow that works best is: use the browser for 90% of UI work, then switch to the simulator for native API testing and layout checks. You won't catch safe-area issues or scroll inertia differences until you're in the actual WebView.

Native Plugins: Camera, Geolocation, and Beyond

This is where Capacitor actually earns its keep. The official plugin collection covers the APIs you'll hit in most apps. Each plugin ships as an npm package with TypeScript types included.

npm install @capacitor/camera @capacitor/geolocation @capacitor/push-notifications
npx cap sync

Using them is straightforward — each plugin exports a typed object you import and call. The same code runs on iOS, Android, and the web (with appropriate fallbacks for the web layer).

import { Camera, CameraResultType } from '@capacitor/camera';

async function takeSelfie() {
  const photo = await Camera.getPhoto({
    quality: 80,
    allowEditing: false,
    resultType: CameraResultType.DataUrl,
  });
  return photo.dataUrl; // use directly in an <img src={...} />
}

Permission handling is built into each plugin — they'll prompt the user on first call. That said, you still need to add the right permission strings to Info.plist on iOS and AndroidManifest.xml on Android. Capacitor puts placeholder comments in those files during cap add, but you need to fill in the actual usage descriptions or Apple will reject your app during review. A missing NSCameraUsageDescription string will get you rejected every time.

For anything not covered by the official plugins, there's a solid community ecosystem, and writing your own plugin is genuinely not that hard. It's a Swift class on iOS and a Kotlin class on Android, plus a TypeScript bridge. The Capacitor docs walk through it clearly.

Styling for Mobile: Safe Areas, Touch Targets, and Scrolling

Your browser CSS doesn't automatically account for the iPhone notch, Dynamic Island, or Android gesture bars. You need to handle safe areas explicitly, or your app will have content hidden behind the status bar.

/* globals.css */
:root {
  --sat: env(safe-area-inset-top);
  --sar: env(safe-area-inset-right);
  --sab: env(safe-area-inset-bottom);
  --sal: env(safe-area-inset-left);
}

.app-header {
  padding-top: calc(16px + var(--sat));
}

.bottom-nav {
  padding-bottom: calc(8px + var(--sab));
}

Touch targets need to be at least 44×44px — that's Apple's HIG number and it's also what Google recommends. If you've got 32px icon buttons in your desktop UI, add min-height: 44px; min-width: 44px on mobile. The Empire UI glassmorphism components are already sized for touch, which saves you from having to audit every interactive element.

Scroll behavior is one of the trickiest differences. Native scroll has momentum (inertia) that the browser's overflow: scroll approximates but doesn't perfectly match. Add -webkit-overflow-scrolling: touch to scrollable containers, and set overscroll-behavior: none on the body to prevent the pull-to-refresh gesture from firing in places where you don't want it.

One thing that trips people up: position: fixed doesn't always work reliably in WebViews when the soft keyboard appears. The keyboard pushes the viewport up, and fixed elements can jump. The safest pattern is to use position: sticky for navigation elements and let the native keyboard handling do its thing.

For visual design in mobile contexts, the glassmorphism generator is worth opening — backdrop-filter effects look great on mobile and the hardware acceleration is solid on devices from 2022 onward. Just watch your blur radius; anything above blur(20px) on a large surface can cause frame drops on mid-range Android devices.

Builds, Signing, and App Store Submission

Getting a build onto the stores involves native signing — it's the part that frustrates most web developers because it requires Apple and Google developer accounts, certificates, and provisioning profiles. There's no way to skip it.

For iOS, you'll work inside Xcode. Open the project with npx cap open ios, select your team under Signing & Capabilities, and let Xcode manage provisioning automatically if you're just testing. For production builds destined for the App Store, use App Store Connect and Archive builds. The minimum deployment target for Capacitor 6 is iOS 14.0.

Android builds use Gradle under the hood. For a release APK or AAB (Google Play requires AAB format now), generate a keystore and sign via ./gradlew bundleRelease. Store your keystore somewhere safe — losing it means you can't update your app on Google Play.

# Generate a release keystore (do this once, store it securely)
keytool -genkey -v -keystore my-release.keystore \
  -alias my-key-alias -keyalg RSA -keysize 2048 -validity 10000

Honestly, the submission process itself — screenshots, metadata, review — is the same as any native app. Capacitor doesn't give you a shortcut there. Build times are also native build times: Xcode Release builds can take 3–8 minutes even for simple apps. Factor that into your CI pipeline if you're automating releases.

One more thing — if you're using live reload in production (pointing the WebView at a remote URL instead of bundled files), both stores will likely reject you. The App Store explicitly bans dynamically downloaded executable code. Bundle your JS with the app.

Pulling It Together: A Real-World Pattern

The apps that work best with Capacitor are ones with a clear separation between the web-first UI and the native feature layer. Keep all your native plugin calls in a single service layer — don't scatter Camera.getPhoto() calls across components. This makes it easy to swap in browser-compatible mocks during development.

// src/services/device.ts
import { Camera, CameraResultType } from '@capacitor/camera';
import { Capacitor } from '@capacitor/core';

export async function capturePhoto(): Promise<string | null> {
  if (!Capacitor.isNativePlatform()) {
    // browser fallback: open file picker
    return null;
  }
  const photo = await Camera.getPhoto({
    quality: 75,
    resultType: CameraResultType.DataUrl,
  });
  return photo.dataUrl ?? null;
}

For UI components, you can absolutely use a component library here. Something like Empire UI works well because the components are pure CSS + React with no native dependencies — they'll render identically in a WebView as they do in Chrome. The gradient generator and design tools are handy for producing assets that look sharp at mobile pixel densities.

State management is the same as any React app — Zustand, Jotai, React Query for server state. Nothing Capacitor-specific needed. Where you do need to think differently is around offline behavior: mobile users lose network connectivity in ways browser users rarely do. React Query's staleTime and gcTime settings plus a cache-first strategy covers most cases.

If you're building something larger, consider splitting your Vite config so you have a browser build and a Capacitor build — different base URLs, different env vars, maybe different entry points. Vite's --mode flag handles this cleanly. That way your browser deployment and your app store build don't share configuration that could cause subtle bugs in either context.

FAQ

Does Capacitor work with Next.js?

Yes, but only in static export mode (output: 'export' in next.config.js). You lose API routes, server components, and ISR. For most Capacitor use cases, plain Vite + React is a cleaner starting point.

How does Capacitor compare to Expo for React developers?

Expo is built on React Native, so you're writing React Native components, not web components. Capacitor uses your existing web React code and runs it in a WebView. If you have a web app already, Capacitor is far less work.

Can I use Tailwind CSS in a Capacitor app?

Completely fine. Tailwind is just CSS at the end of the day, and the WebView renders it the same way a browser does. Just make sure your build output includes the compiled CSS file.

Will Apple reject my Capacitor app?

Not for using Capacitor specifically — plenty of apps in the App Store use it. You can get rejected for missing permission strings, using live remote execution, or violating content guidelines, same as any other app.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Tauri + React: Build Desktop Apps With Web TechnologiesReact Native in 2026: New Architecture, Expo 52 and BridgelessSidebar Navigation in React: Collapsible, Mobile Drawer, NestedFloating Action Button (FAB) in React: Mobile-First Speed Dial