from a web application to a real iOS and Android app
Vue Capacitor connects an existing Vue application to a native runtime without splitting the codebase apart. Camera, geolocation, push notifications, and filesystem access are reached through one unified JavaScript API, while the app ships as a real iOS and Android binary to each store.
Table of Contents
- 1. Why Vue Capacitor instead of a separate mobile codebase
- 2. Project setup: embedding a Vue app in Capacitor
- 3. Using native APIs: camera, geolocation, filesystem
- 4. Plugin architecture and writing custom native plugins
- 5. Build and deployment for iOS and Android
- 6. App lifecycle: pause, resume, and deep links
- 7. Optimizing performance inside the WebView
- 8. Debugging on real devices
- 9. Vue Capacitor versus other cross platform approaches
- 10. Summary
- 11. FAQ
1. Why Vue Capacitor instead of a separate mobile codebase
Anyone already running a Vue application in the browser who also wants to offer a mobile app faces a fundamental choice: write an entirely new native codebase in Swift and Kotlin, adopt a cross platform framework like React Native or Flutter, or wrap the existing Vue application in a native shell. This is exactly where Vue Capacitor comes in. Capacitor is a native runtime that turns a web application into a real native app without rewriting components, business logic, or styling.
The decisive difference from classic WebView wrappers lies in the bridge architecture. Vue Capacitor exposes a typed JavaScript API through which native functionality such as camera, push notifications, or biometrics is called directly from Vue code. The app does not run inside a browser tab but inside a standalone, system native WebView with full access to native capabilities, and it is published to the App Store or Play Store as a regular binary.
For teams with existing Vue expertise, this is a major advantage: the learning curve stays low because components, composables, and the entire build process still rely on Vite and Vue 3. At the same time, the door to native capability stays open, because Capacitor is not a closed system but an open plugin architecture that can be extended with custom native code whenever needed. This combination of web productivity and native extensibility is what makes Vue Capacitor a realistic alternative for teams that do not want to maintain two separate codebases.
2. Project setup: embedding a Vue app in Capacitor
Getting started with Vue Capacitor does not begin from scratch, but from an existing or new Vue 3 application built with Vite. Capacitor itself is deliberately framework agnostic: it simply expects a build directory with static HTML, CSS, and JavaScript files that it copies into the native shell. That makes integrating it into an existing Vue project a matter of a few steps, without touching the current project structure.
After installing the core Capacitor packages, a configuration file is created that defines the app ID, app name, and build directory. The native platform projects for iOS and Android are then generated, landing in the repository as standalone Xcode and Android Studio projects respectively. These platform folders contain native configuration files such as Info.plist or AndroidManifest.xml, which can be adjusted directly whenever needed, for example for permissions or app icons.
# Capacitor core and CLI added to an existing Vue 3 + Vite project
npm install @capacitor/core
npm install -D @capacitor/cli
# Initialize Capacitor: app name, bundle ID, web dir
npx cap init "Mironsoft App" "de.mironsoft.app" --web-dir=dist
# Add native platform projects (creates ios/ and android/ folders)
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
# Build the Vue app, then sync web assets + plugins into native projects
npm run build
npx cap sync
A central piece of Vue Capacitor is capacitor.config.ts. Besides app ID and web directory, it also configures server options for local development, splash screen behavior, and plugin specific settings. Anyone who wants live reload on a real device during development enters the local IP address of the development machine here, so the native app loads directly against the Vite dev server instead of a static build directory.
3. Using native APIs: camera, geolocation, filesystem
The practical value of Vue Capacitor shows up when accessing native device functionality. Instead of browser APIs such as navigator.geolocation or getUserMedia, whose reliability varies by platform and browser version, the corresponding Capacitor plugins are called instead. These wrap the native implementation on iOS and Android behind one unified, promise based JavaScript API that integrates smoothly into Vue composables.
Every native API additionally requires declaring the matching permission in the platform configuration files. Without a corresponding entry in Info.plist for iOS or AndroidManifest.xml for Android, the operating system either denies access or the app crashes on the call. This step is often overlooked in tutorials, but it is mandatory for a production use of Vue Capacitor.
// composables/useCamera.js
import { ref } from 'vue'
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'
import { Geolocation } from '@capacitor/geolocation'
import { Filesystem, Directory } from '@capacitor/filesystem'
export function useCamera() {
const photoUrl = ref(null)
const error = ref(null)
async function takePhoto() {
try {
const photo = await Camera.getPhoto({
resultType: CameraResultType.Uri,
source: CameraSource.Camera,
quality: 80,
})
photoUrl.value = photo.webPath
} catch (err) {
error.value = err.message
}
}
async function getCurrentPosition() {
const coordinates = await Geolocation.getCurrentPosition({
enableHighAccuracy: true,
timeout: 10000,
})
return { lat: coordinates.coords.latitude, lng: coordinates.coords.longitude }
}
async function saveNote(filename, content) {
await Filesystem.writeFile({
path: filename,
data: content,
directory: Directory.Documents,
encoding: 'utf8',
})
}
return { photoUrl, error, takePhoto, getCurrentPosition, saveNote }
}
Important with Vue Capacitor: every plugin call is asynchronous and returns a promise, which combines well with async/await inside composables. Error handling should always be explicit, because denied permissions, missing hardware, or network problems occur far more often on real devices than in browser testing. A composable like useCamera cleanly encapsulates this complexity and makes native functionality reusable across every component.
4. Plugin architecture and writing custom native plugins
The official plugin library covers the most common use cases, but it does not cover every requirement. This is exactly where the strength of the Vue Capacitor plugin architecture shows: a custom plugin consists of a TypeScript definition, a web implementation as a browser fallback, and native code in Swift for iOS or Kotlin for Android. All three implementations share the same API interface, so the Vue code never needs to know which platform it is running on.
A typical use case for a custom plugin is integrating an in house SDK, for example for payment processing or a hardware scanner, that does not exist as an official Capacitor plugin. The effort for a minimal plugin is manageable: on iOS a class is created that inherits from CAPPlugin and exposes methods with the @objc attribute. On Android the plugin class inherits from Plugin and registers methods through the @PluginMethod annotation.
// plugins/definitions.ts — shared TypeScript interface
export interface DeviceScannerPlugin {
scanBarcode(): Promise<{ value: string; format: string }>
isSupported(): Promise<{ supported: boolean }>
}
// plugins/index.ts — plugin registration for web/native
import { registerPlugin } from '@capacitor/core'
import type { DeviceScannerPlugin } from './definitions'
const DeviceScanner = registerPlugin<DeviceScannerPlugin>('DeviceScanner', {
web: () => import('./web').then((m) => new m.DeviceScannerWeb()),
})
export default DeviceScanner
// usage inside a Vue component's setup()
import DeviceScanner from '@/plugins'
async function scan() {
const { supported } = await DeviceScanner.isSupported()
if (!supported) return
const result = await DeviceScanner.scanBarcode()
console.log(result.value, result.format)
}
This separation between interface and implementation is the core of the Vue Capacitor plugin architecture. It allows developing a plugin with only a web fallback first, and adding the native implementation later once a real device becomes available for testing. For teams that are mostly Vue developers with only occasional native specialists, this significantly reduces coordination overhead, because both sides can work independently against the same interface.
5. Build and deployment for iOS and Android
The build process of Vue Capacitor runs in two clearly separated phases. First, Vite builds the Vue application into static assets as usual. Then npx cap sync copies those assets, along with every installed plugin, into the native platform projects and updates their dependencies. Only after that do Xcode and Android Studio come into play to produce a signed binary from the native project.
For continuous delivery, it pays off to clearly separate the web build from the native build in the CI pipeline. The web build can run on every commit and is fast, while the native build with code signing and app store upload takes considerably more time and credentials, and is typically triggered only on release tags. Fastlane has become the standard tool in practice to automate signing, versioning, and upload to App Store Connect or Play Console.
# Standard release workflow for a Vue Capacitor app
npm run build # Vite build → dist/
npx cap sync ios android # copy web assets + plugins into native projects
npx cap open ios # opens Xcode for signing, archiving, upload
npx cap open android # opens Android Studio for signed bundle
# CI: automate versioning before native build
npx cap sync
cd ios/App && fastlane release # bumps build number, archives, uploads to TestFlight
cd android && fastlane deploy # bumps versionCode, builds AAB, uploads to Play Console
A common mistake when deploying Vue Capacitor apps: developers forget that changes to capacitor.config.ts or to native plugin configuration require a fresh cap sync before they become visible in the Xcode or Android Studio project. Simply rebuilding the Vue application is not enough, because sync and build are two separate steps in the deployment process that are frequently confused.
6. App lifecycle: pause, resume, and deep links
Native apps have a lifecycle that does not exist in this form in the browser: an app can be paused when the user sends it to the background, and later wake up again without JavaScript state being lost. Vue Capacitor provides the App plugin for exactly this, firing events like pause, resume, and appUrlOpen whenever the native lifecycle status changes.
The appUrlOpen event is especially relevant for deep links. When the app is opened through a Universal Link on iOS or an App Link on Android, this event delivers the full URL, which can then be matched against the Vue Router to navigate directly to the correct view. Without this wiring, the user always lands on the app start screen when opening a link, regardless of which content was actually linked.
// main.js — wiring native lifecycle events into the Vue Router
import { App as CapacitorApp } from '@capacitor/app'
import router from './router'
CapacitorApp.addListener('appUrlOpen', (event) => {
// event.url e.g. "https://app.mironsoft.de/products/42"
const slug = new URL(event.url).pathname
router.push(slug)
})
CapacitorApp.addListener('pause', () => {
// persist unsaved form state before the OS may suspend the process
localStorage.setItem('draft-state', JSON.stringify(currentDraft.value))
})
CapacitorApp.addListener('resume', () => {
// re-fetch data that might be stale after a long background phase
refreshDashboard()
})
In practice, it pays off to always persist critical user input on the pause event, because the operating system can terminate a backgrounded app at any time without warning to free memory. Anyone relying on Vue Capacitor purely on Vue's normal reactivity cycle risks data loss at exactly these moments, which are barely reproducible in browser testing.
7. Optimizing performance inside the WebView
A native shell does not automatically make a slow web application fast. The WebView in which Vue Capacitor apps run has noticeably less compute power on older Android devices than a modern desktop browser, which means bundle size and rendering performance directly influence perceived app quality. Code splitting through dynamic imports in the Vue Router reduces the amount of initial JavaScript that must be parsed at app start.
A second important lever concerns communication over the native bridge. Every call to a Capacitor plugin travels across a bridge between JavaScript and native code, which creates a small but measurable overhead compared to pure JavaScript. Frequent, small bridge calls in a loop, for example writing many individual files, add up to noticeable delays. Batching such operations into a single plugin call significantly reduces the number of bridge round trips.
// router/index.js — code splitting reduces initial WebView parse time
const routes = [
{
path: '/dashboard',
component: () => import('@/views/DashboardView.vue'),
},
{
path: '/settings',
component: () => import('@/views/SettingsView.vue'),
},
]
// AVOID: many small bridge round trips in a loop
for (const item of items) {
await Filesystem.writeFile({ path: item.path, data: item.data, directory: Directory.Cache })
}
// BETTER: batch into a single native call when the plugin supports it,
// or write one combined JSON file instead of many small ones
await Filesystem.writeFile({
path: 'cache/batch.json',
data: JSON.stringify(items),
directory: Directory.Cache,
})
It also helps to convert images and other assets to modern formats such as WebP already within the Vue build, and to serve them at sizes appropriate for mobile devices. Because Vue Capacitor apps rely on the same Vite build process as the web version, existing optimizations from the web project, such as lazy loading images or tree shaking unused libraries, carry over directly, without needing a separate mobile specific build configuration.
8. Debugging on real devices
Debugging Vue Capacitor apps differs from familiar browser debugging, because bugs often surface only on real devices, especially with native plugin interactions. On Android, the running WebView can be inspected through chrome://inspect in Chrome DevTools once the device is connected via USB and developer options are enabled. That gives full access to console, network tab, and Vue DevTools, exactly as in the browser.
On iOS, the Safari Web Inspector takes over this role: after enabling the web inspector option in iOS settings, the running app appears under Safari on a Mac under Develop and can be inspected there as well. Live reload against the Vite dev server, configured through the server.url option in capacitor.config.ts, further speeds up the development cycle significantly, because changes to Vue code appear on the device immediately without a fresh native build.
{
"appId": "de.mironsoft.app",
"appName": "Mironsoft App",
"webDir": "dist",
"server": {
"url": "http://192.168.1.42:5173",
"cleartext": true
},
"android": {
"webContentsDebuggingEnabled": true
}
}
This server configuration should only ever exist for local development and must never end up in a production build, because it forces the app to load against a fixed IP address on the local network. A separate build profile or a conditional configuration per environment prevents this setting from accidentally reaching an app store release.
9. Vue Capacitor versus other cross platform approaches
The decision for Vue Capacitor should be made in the context of the alternatives, because each approach makes different trade offs between development speed, native performance, and code reuse.
| Approach | Codebase | Native performance | Onboarding for Vue teams |
|---|---|---|---|
| Vue Capacitor | 100 percent shared (WebView) | Good, WebView based | Very low, existing Vue skills |
| React Native | Shared logic, native views | Very good, native UI components | Requires a new framework |
| Flutter | 100 percent shared (Dart) | Very good, own render engine | New language and new framework |
| NativeScript Vue | Vue templates, native views | Very good, direct native APIs | Medium, different rendering logic |
| Cordova (legacy) | 100 percent shared (WebView) | Adequate, older bridge | Low, but outdated architecture |
For teams with an existing Vue application and a limited budget for native development, Vue Capacitor is the most pragmatic choice in most cases, because it fully reuses the existing codebase and only hits native limits where pixel perfect native UI components are genuinely required, for example complex lists with native scroll physics. React Native and Flutter deliver better results in such special cases, but require either learning a new UI layer or an entirely new framework.
Mironsoft
Vue development, cross platform apps, and native integrations
Want your Vue application as a native app in the store?
We bring existing Vue applications to iOS and Android with Capacitor, integrate native functionality, and build custom native plugins for special requirements when needed.
Capacitor setup
Turn an existing Vue app into a native iOS and Android shell
Native plugins
Custom Swift and Kotlin plugins for requirements without a standard solution
Store deployment
Set up a Fastlane pipeline for App Store and Play Store
10. Summary
Vue Capacitor solves a concrete problem: shipping an existing Vue application as a real native app on iOS and Android without duplicating the codebase. The native bridge exposes camera, geolocation, filesystem, and further device functionality through one unified JavaScript API, while custom plugins close gaps that the standard plugin library does not cover. Build and deployment run in two clearly separated phases, a web build via Vite and a native build via Xcode and Android Studio.
The app lifecycle with pause and resume events as well as deep link handling requires extra attention compared to a pure web application, as does performance tuning for the WebView environment on older devices. Anyone who accounts for these points from the start gets a maintainable, production ready foundation for mobile apps with Vue Capacitor that uses the same codebase as the web version while still retaining full access to native platform features.
Vue Capacitor — the essentials at a glance
Setup
npx cap init and npx cap add ios android generate native platform projects from an existing Vite Vue app.
Native APIs
Camera, geolocation, and filesystem run through promise based plugins, wrapped in Vue composables.
Custom plugins
A TypeScript interface plus Swift and Kotlin implementations close gaps in the standard library.
Lifecycle & performance
Pause/resume events preserve state, code splitting and batched bridge calls keep the WebView fast.