Capacitor 7: Web to Native Apps
HOW TO GUIDES Feb. 6, 2026, 11:30 p.m.

Capacitor 7: Web to Native Apps

Capacitor 7 bridges the gap between modern web development and native mobile experiences, letting you ship a single codebase as a fully‑featured iOS, Android, or even desktop app. If you’ve ever wished your React, Vue, or plain HTML‑CSS‑JS project could live on a phone without rewriting everything, you’re in the right place. In this guide we’ll walk through the whole lifecycle—from a vanilla web app to a polished native build—while sprinkling in real‑world examples and pro tips you can apply today.

What’s New in Capacitor 7?

Capacitor 7 arrives with a leaner core, first‑class support for the latest iOS 17 and Android 14 SDKs, and an upgraded plugin system that embraces ES modules out of the box. The new @capacitor/cli commands are faster, and the auto‑linking of native dependencies means you spend less time tweaking Gradle or Xcode project files.

Another highlight is the Live Reload feature for native builds. When you edit your web assets, Capacitor now pushes the changes directly to the device, cutting the compile‑run loop from minutes to seconds. This makes iterative UI work feel as snappy as web development.

Setting Up the Development Environment

Before you dive in, make sure you have Node.js ≥ 18, the latest npm or yarn, and the native SDKs for the platforms you target. On macOS, Xcode 15 and Android Studio Flamingo are the recommended versions.

Install Capacitor globally and create a fresh project with your favorite framework. Here’s a minimal setup using vanilla JavaScript:

npm init -y
npm install @capacitor/core @capacitor/cli
npx cap init my-capacitor-app com.example.myapp

After initialization, Capacitor creates a capacitor.config.ts (or .json) file where you define app metadata, server URLs, and platform-specific options. Keep this file version‑controlled; it’s the single source of truth for both web and native builds.

Creating the Web Layer

The web layer is where most of your business logic lives. Whether you use React, Vue, or plain HTML, Capacitor serves the www folder as the native WebView content. Let’s build a tiny note‑taking app to illustrate the workflow.

HTML Structure

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Capacitor Notes</title>
  <style>
    body { font-family: sans-serif; margin: 1rem; }
    #list { margin-top: 1rem; }
  </style>
</head>
<body>
  <h1>My Notes</h1>
  <input id="noteInput" placeholder="Write a note..." />
  <button id="addBtn">Add</button>
  <ul id="list"></ul>
  <script src="app.js"></script>
</body>
</html>

JavaScript Logic (app.js)

const listEl = document.getElementById('list');
const inputEl = document.getElementById('noteInput');
const addBtn = document.getElementById('addBtn');

function render(notes) {
  listEl.innerHTML = '';
  notes.forEach((note, i) => {
    const li = document.createElement('li');
    li.textContent = note;
    li.onclick = () => deleteNote(i);
    listEl.appendChild(li);
  });
}

function loadNotes() {
  const stored = localStorage.getItem('notes');
  return stored ? JSON.parse(stored) : [];
}

function saveNotes(notes) {
  localStorage.setItem('notes', JSON.stringify(notes));
}

function addNote() {
  const note = inputEl.value.trim();
  if (!note) return;
  const notes = loadNotes();
  notes.push(note);
  saveNotes(notes);
  render(notes);
  inputEl.value = '';
}

function deleteNote(index) {
  const notes = loadNotes();
  notes.splice(index, 1);
  saveNotes(notes);
  render(notes);
}

addBtn.addEventListener('click', addNote);
render(loadNotes());

At this point you can run npx http-server ./ (or any static server) and see the note app in the browser. All data lives in localStorage, which Capacitor will automatically map to native storage on the device.

Adding Native Functionality with Plugins

Capacitor’s power shines when you start tapping into native APIs. The framework ships with a set of core plugins—Camera, Geolocation, Filesystem, etc.—and you can also write custom plugins in Swift/Kotlin.

Let’s extend our note app to take a photo and attach it to a note using the @capacitor/camera plugin.

npm install @capacitor/camera
npx cap sync

Update app.js to import and use the plugin:

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

async function addPhotoNote() {
  const image = await Camera.getPhoto({
    quality: 90,
    allowEditing: false,
    resultType: CameraResultType.DataUrl,
    source: CameraSource.Camera,
  });

  const note = prompt('Enter a caption for the photo:');
  if (!note) return;

  const notes = loadNotes();
  notes.push({ text: note, img: image.dataUrl });
  saveNotes(notes);
  render(notes);
}

// Adjust render() to handle objects with img property
function render(notes) {
  listEl.innerHTML = '';
  notes.forEach((note, i) => {
    const li = document.createElement('li');
    if (typeof note === 'string') {
      li.textContent = note;
    } else {
      const img = document.createElement('img');
      img.src = note.img;
      img.style.maxWidth = '100px';
      li.appendChild(img);
      li.appendChild(document.createTextNode(' ' + note.text));
    }
    li.onclick = () => deleteNote(i);
    listEl.appendChild(li);
  });
}

Now the web app can capture photos on both browsers (via the MediaDevices API) and native devices (via the Camera plugin). The same code runs everywhere, which is the essence of “write once, run everywhere.”

Preparing Native Projects

Capacitor treats each platform as a separate native project that lives in the android and ios folders. Adding a platform is as simple as:

npx cap add android
npx cap add ios

After adding, run npx cap sync to copy your web assets into the native projects and install any plugin dependencies.

For Android, open the project in Android Studio (android/ folder). For iOS, launch Xcode with npx cap open ios. Both IDEs will now show a minimal native wrapper that loads your index.html inside a WebView.

Building and Running on Devices

With the native projects set up, you can compile and run them just like any other app. Capacitor 7’s CLI now supports a streamlined “run” command that boots the emulator or connects to a physical device.

# Android
npx cap run android --no-open

# iOS
npx cap run ios --no-open

The --no-open flag prevents the IDE from launching automatically, useful when you prefer to control the build process manually. If you have Live Reload enabled, any change you make to the www folder will instantly refresh the app on the device.

Pro Tip: Keep npm run build (or your framework’s production build) in a watch mode while you develop native features. Capacitor’s live‑reload will push the updated bundle without needing a full reinstall.

Real‑World Use Case: Offline‑First Field Survey App

Imagine a field‑service company that needs to collect equipment readings in remote locations with spotty connectivity. An offline‑first Capacitor app can store data locally, sync when a network appears, and still leverage native features like barcode scanning and GPS.

Key Features

  • Local persistence using @capacitor/storage (encrypted on iOS/Android).
  • Barcode scanning via @capacitor-community/barcode-scanner.
  • Background sync with BackgroundFetch plugin.
  • Push notifications for task assignments.

Below is a concise snippet that demonstrates scanning a barcode and saving the result offline:

import { BarcodeScanner } from '@capacitor-community/barcode-scanner';
import { Storage } from '@capacitor/storage';

async function scanAndSave() {
  await BarcodeScanner.checkPermission({ force: true });
  const result = await BarcodeScanner.startScan();
  if (result.hasContent) {
    const records = JSON.parse(await Storage.get({ key: 'records' }) ?? '[]');
    records.push({ barcode: result.content, timestamp: Date.now() });
    await Storage.set({ key: 'records', value: JSON.stringify(records) });
    alert('Saved! Total records: ' + records.length);
  }
}

When the device regains connectivity, a background task can post the accumulated records to your server API. The same JavaScript runs on the web, letting field agents use a browser version when they’re at the office.

Testing and Debugging Strategies

Capacitor provides two main debugging avenues: the browser DevTools for the web layer, and the native platform debuggers for deeper inspection. For Android, use Chrome’s chrome://inspect page; for iOS, Safari’s Web Inspector works the same way.

When you hit platform‑specific bugs, the Capacitor.log API is handy. It forwards messages to the native console, making it easy to trace issues from JavaScript to Swift/Kotlin.

import { Capacitor } from '@capacitor/core';

Capacitor.log({ message: 'App started', level: 'info' });

Unit testing the web code remains unchanged—use Jest or Vitest as you would for any SPA. For native plugins, write platform‑specific tests in Xcode or Android Studio and run them as part of your CI pipeline.

Publishing to App Stores

Before you submit, make sure you bump the version numbers in capacitor.config.ts, android/app/build.gradle, and the Xcode project’s Info.plist. Capacitor 7 can auto‑increment builds with the npx cap version command.

npx cap version --bump patch   # e.g., 1.2.3 → 1.2.4

Generate signed binaries:

  • Android: ./gradlew assembleRelease inside the android folder.
  • iOS: Archive in Xcode and export an .ipa using the App Store Connect workflow.

Don’t forget to enable the required permissions (camera, storage, background fetch) in the platform manifest files. A missing permission is a common reason for rejections during the review process.

Pro Tip: Use npx cap copy after every asset change before building the release. It guarantees that the latest www bundle is baked into the native package.

Advanced Topics: Custom Native Plugins

If the core plugins don’t cover a specific device capability—say, a proprietary Bluetooth sensor—you can write a custom plugin. Capacitor’s plugin template scaffolds both the JavaScript interface and the native stub files.

npx @capacitor/create-plugin my-sensor
cd my-sensor
npm run build
npm link   # optional for local testing

In the generated src/ios folder, add Swift code that interacts with CoreBluetooth. In src/android, implement the same logic in Kotlin. Finally, expose the methods via a TypeScript definition file so your web code can call MySensor.read() just like any other plugin.

Performance Optimizations

Even though Capacitor runs your UI in a WebView, you can achieve near‑native performance with a few tricks. First, enable prefers-color-scheme media queries to let the OS handle dark mode without JavaScript overhead. Second, lazy‑load heavy assets using the loading="lazy" attribute on images.

On the native side, configure the WebView’s WKWebViewConfiguration (iOS) or WebSettings (Android) to enable hardware acceleration and disable unnecessary debugging flags in production builds.

Security Best Practices

Because your app runs HTML/JS, treat it like any web application: sanitize user input, use CSP headers (if you serve from a remote URL), and avoid eval‑style constructs. Capacitor also offers a SecureStorage plugin for storing secrets such as API tokens.

import { SecureStoragePlugin } from '@capacitor/secure-storage';

await SecureStoragePlugin.set({ key: 'apiToken', value: token });
const saved = await SecureStoragePlugin.get({ key: 'apiToken' });

Combine SecureStorage with native biometric authentication (Face ID/Touch ID) for an extra layer of protection on sensitive screens.

Conclusion

Capacitor 7 empowers developers to turn any modern web app into a native experience without sacrificing the agility of the web ecosystem. By mastering the workflow—from web assets to native plugins, live‑reload development, offline‑first strategies, and polished publishing—you can deliver cross‑platform solutions that feel truly native. Whether you’re building a simple note‑taker or a complex field‑service platform, the tools and patterns covered here will help you ship faster, iterate smarter, and keep your codebase maintainable for the long haul.

Share this article