Turning a Vue.js or Nuxt app into a mobile app with Capacitor
By Nathan Guigui, Founder of GUIGUI Tech · Facts checked against the official sources listed at the end of the article
Yes, a Vue.js or Nuxt app can become a real iOS and Android app without a rewrite: Capacitor packages your web build inside a native project and exposes device features through plugins. For Vue 3 with Vite, you install Capacitor, point its webDir at dist and generate the iOS and Android projects. For Nuxt there is one extra condition: the app must be generated as static files (nuxt generate, ideally with ssr: false), because no Node server runs on the phone.
What do you need to use Capacitor with Vue or Nuxt?
Capacitor 8, the stable version as of October 2026, requires Node.js 22 or later, a Mac with Xcode 26.0 or later for iOS, and Android Studio 2025.2.1 or later for Android. According to the Capacitor 8 upgrade guide, projects target at least iOS 15.0 and Android API 24.
| Item (Capacitor 8, October 2026) | Requirement |
|---|---|
| Node.js | 22 or later |
| Xcode (iOS) | 26.0 or later, so a Mac |
| Android Studio | 2025.2.1 (Otter) or later |
| Minimum iOS target | iOS 15.0 |
| Android | minSdkVersion 24, targetSdkVersion 36 |
| iOS dependencies | Swift Package Manager by default |
On the code side, you need a Vue 3 project that produces a static build, which is what Vite does by default. Ionic, the company behind Capacitor, forecasts Capacitor 9 for the end of November 2026 in The Road to Capacitor 9, and notes that the CocoaPods trunk becomes permanently read-only on December 2, 2026: stick with Swift Package Manager.
How do you add Capacitor to a Vue 3 + Vite project?
To add Capacitor to a Vue 3 + Vite project, install @capacitor/core and @capacitor/cli, run npx cap init with dist as the web folder, add the platforms, then chain the Vite build and npx cap sync. The commands follow the official installation guide:
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
npx cap add ios
npx cap add android
npm run build
npx cap sync
npx cap run ios
The identifier (com.example.myapp) becomes the iOS bundle ID and the Android application ID, and cannot change once the app is published. npx cap init creates a capacitor.config.ts:
import type { CapacitorConfig } from '@capacitor/cli'
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My App',
webDir: 'dist',
}
export default config
dist is the default value of Vite's build.outDir option; if you changed it, use the same value in webDir. The ios/ and android/ folders are real native projects and belong in Git.
npx cap sync copies the build into the native projects and updates plugin dependencies: run it again after every web build and every new plugin.
Key point
Add a "build:mobile": "vite build && cap sync" script to your package.json. Forgetting the sync step is the most common cause of "my changes don't show up on the phone".
How do you configure Nuxt for Capacitor?
To use Capacitor with Nuxt, produce a fully static build with nuxt generate (or npx nuxi generate) and set .output/public as the webDir. Capacitor only ships files: there is no Nitro server on the phone.
The Nuxt deployment documentation describes two static options: prerendering every route (ssr: true, the default behavior of nuxt generate) or client-side only rendering (ssr: false), which outputs .output/public/index.html and JavaScript bundles, like a classic Vue app. For a mobile app, ssr: false is the simplest choice: prerendering mostly helps SEO, and SPA mode avoids running code in a server context where Capacitor APIs do not exist.
// nuxt.config.ts
export default defineNuxtConfig({
ssr: false,
})
In capacitor.config.ts, set webDir: '.output/public'. If the same project also serves the website with SSR, enable ssr: false only for the mobile build, through an environment variable read in nuxt.config.ts.
Server routes do not come along. According to the rendering modes page, nuxt generate includes no server in the output folder, so no server endpoint can be used. Everything in server/api/ has to be deployed elsewhere and called with an absolute URL. Without a server, runtimeConfig.public values (such as the API URL) must be set when you run nuxt generate.
Should you use history mode or hash mode with Vue Router?
Vue Router's history mode (createWebHistory) generally works in a Capacitor app and remains the default choice. The app is served from a local origin (capacitor://localhost on iOS, https://localhost on Android, according to the configuration defaults) and always starts on index.html. Hash mode (createWebHashHistory), which according to the Vue Router documentation needs no server configuration, remains a fallback.
Keeping history mode also lets deep links use the same paths as your website: Universal Links on iOS (an apple-app-site-association file), App Links on Android (an assetlinks.json file). Once the setup described in Capacitor's deep linking guide is in place, the Vue side only needs to listen to the appUrlOpen event:
// src/main.ts, after app.use(router)
import { App as CapApp } from '@capacitor/app'
CapApp.addListener('appUrlOpen', (event) => {
const url = new URL(event.url)
router.push(url.pathname + url.search)
})
In Nuxt, put this code in a client plugin (plugins/deep-links.client.ts) and get the router with useRouter().
How do you use Capacitor native plugins from Vue?
Capacitor plugins are installed with npm, imported in TypeScript and called like async functions: the cleanest approach is to wrap them in Vue composables. Run npx cap sync after each install.
| Need | Plugin | Watch out for |
|---|---|---|
| Photos, gallery | @capacitor/camera | Permission strings in Info.plist |
| Push notifications | @capacitor/push-notifications | Push capability on iOS, google-services.json on Android |
| Key-value storage | @capacitor/preferences | Not a database, not encrypted |
| Links, back button | @capacitor/app | Android back button to handle |
| Keyboard | @capacitor/keyboard | Resize behavior on iOS |
Since version 8.1.0 of the Camera plugin, the documentation recommends takePhoto and chooseFromGallery, which replace the now deprecated getPhoto and pickImages:
// src/composables/usePhoto.ts
import { ref } from 'vue'
import { Camera, CameraDirection } from '@capacitor/camera'
export function usePhoto() {
const photoUrl = ref<string | null>(null)
async function takePhoto() {
const result = await Camera.takePhoto({
quality: 90,
cameraDirection: CameraDirection.Rear,
})
photoUrl.value = result.webPath ?? null
}
return { photoUrl, takePhoto }
}
On Android, listening to the backButton event of the App plugin disables the default behavior: it is up to you to go back or exit the app.
// src/composables/useAndroidBack.ts
import { onMounted, onUnmounted } from 'vue'
import { useRouter } from 'vue-router'
import { App as CapApp } from '@capacitor/app'
import type { PluginListenerHandle } from '@capacitor/core'
export function useAndroidBack() {
const router = useRouter()
let handle: PluginListenerHandle | undefined
onMounted(async () => {
handle = await CapApp.addListener('backButton', ({ canGoBack }) => {
if (canGoBack) router.back()
else CapApp.exitApp()
})
})
onUnmounted(() => handle?.remove())
}
The Preferences plugin replaces localStorage, which the operating system may clear, for small persistent data; it encrypts nothing, so keep secrets out of it. For push notifications, order matters: requestPermissions(), then register(), then the registration event to get the token you send to your backend. Finally, check Capacitor.isNativePlatform() (from @capacitor/core) before calling a plugin with no web implementation, so the same code keeps running on the website.
How do you develop with live reload on a phone?
Live reload makes the app load your computer's development server instead of the bundled files: every change shows up on the phone without a rebuild. The server must listen on the local network and the phone must be on the same Wi-Fi.
npx vite --host
npx cap run android --live-reload --host 192.168.1.20 --port 5173
Vite's --host option listens on all addresses; replace the IP with your computer's. The npx cap run options are described in the CLI documentation. With Nuxt, run npx nuxi dev --host and use port 3000. The live reload guide also describes a variant with server.url in the config, with a warning not to commit it: an app published that way would load your computer.
What are the common pitfalls of a Vue app with Capacitor?
The most frequent pitfalls are relative API URLs and CORS, authentication cookies, screen safe areas, the keyboard, icons and iOS permission strings. They often surface late, on a real device.
API URLs and CORS. Every API URL must be absolute. Your server sees requests coming from capacitor://localhost or https://localhost: add these origins to its CORS configuration, or enable CapacitorHttp, disabled by default, which routes fetch and XMLHttpRequest through native HTTP libraries.
Cookies and authentication. A cookie-based session can break, because the API domain is no longer the page's domain. The CapacitorCookies documentation notes that since iOS 14, third-party cookies are not available by default, and suggests declaring your domains in WKAppBoundDomains. A token sent in a header is often easier to make reliable.
Safe areas. Add viewport-fit=cover to the viewport meta tag and use env(safe-area-inset-top) and its variants. On Android, the System Bars plugin, bundled with Capacitor 8, also injects --safe-area-inset-* variables for older WebViews.
Keyboard. On iOS, the Keyboard plugin sets how the app resizes (native, body or none). native mode affects vh units: test forms at the bottom of the screen.
Icons and splash screen. npx capacitor-assets generate produces every size from icons of at least 1024 × 1024 px and splash screens of at least 2732 × 2732 px, according to the documentation.
iOS permission strings. Every access to the camera, photos or location requires a string in Info.plist. According to Apple, without it the access fails and may crash the app:
<key>NSCameraUsageDescription</key>
<string>Take photos of your receipts to attach them to your file.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Choose an existing photo to attach to your file.</string>
Guideline 5.1.1(ii) of the App Review Guidelines requires these strings to clearly and completely describe how the data is used: avoid "for the app".
How do you publish a Vue app on the App Store and Google Play?
To publish a Vue app built with Capacitor, you sign the native projects in Xcode and Android Studio, then submit them through App Store Connect and the Google Play Console. As of October 2026, Apple has required Xcode 26 and the iOS 26 SDK since April 28, 2026 (Apple Developer), and Google Play has required targeting Android 16 (API 36) since August 31, 2026 (Android Developers); Capacitor 8 already targets API 36.
The main risk is not technical. If the app only reproduces the website, Apple can reject it under Guideline 4.2 "Minimum Functionality", which asks for more than a repackaged website. We cover this rejection in our article on the Apple 4.2 Minimum Functionality rejection. Before the first submission, check at least:
- mobile navigation (tab bar, consistent back button), without the website's menu or footer;
- a few genuinely useful native features: notifications, camera, biometric sign-in, data available offline;
- a clear screen when there is no network;
- in-app account deletion if the app lets users create an account (Guideline 5.1.1(v));
- a demo account for Apple's review team.
Capacitor or React Native for a Vue team?
For a Vue team that already has a working web app, Capacitor is usually the most direct choice: it reuses the code, the skills and often the screens. React Native means learning React and rewriting the UI with native components. That cost mostly pays off when the mobile app needs things a WebView handles poorly: very demanding animations, very long lists, heavy native usage, or a mobile UI that differs a lot from the website. The right choice depends on how mature your web app is and what the mobile app needs to do on top of it.
Getting help turning your Vue or Nuxt app into a mobile app
GUIGUI Tech offers the Website → Mobile App (Capacitor) service from €8,500 excl. VAT, in 3 to 6 weeks: your Vue, Nuxt (SPA/SSG mode), React or Next.js app turned into an iOS and Android app, with icons, splash screen, setup and submission to the App Store and Google Play. If your app already exists on Cordova or Ionic, see the Cordova / Ionic to Capacitor Migration, from €9,500 excl. VAT. If Apple has already rejected your app, the App rejected by Apple: we get it approved offer starts with a €500 excl. VAT diagnosis, delivered in 2 business days.
Frequently asked questions
Can a Vue.js app be turned into a mobile app?
Does Capacitor work with Nuxt?
Which webDir should I use for Capacitor with Vue and Vite?
Do I need to switch Vue Router to hash mode for Capacitor?
Which Node and Xcode versions does Capacitor need in October 2026?
Can Apple reject a Vue app wrapped with Capacitor?
Need help with this?
Website → Mobile App (Capacitor)
Your React, Next.js or Vue.js website turned into a real iOS and Android app, installable from the stores. Capacitor expertise proven in production.
Learn more →
Sources
- Installing Capacitor (Capacitor v8 documentation)
- Environment Setup (Capacitor v8 documentation)
- Updating to Capacitor 8 (Capacitor documentation)
- The Road to Capacitor 9, August 28, 2026 (Ionic blog)
- Capacitor Configuration (Capacitor documentation)
- npx cap run command (Capacitor documentation)
- Live Reload (Capacitor documentation)
- Deep Linking with Universal and App Links (Capacitor documentation)
- Camera plugin (Capacitor documentation)
- Push Notifications plugin (Capacitor documentation)
- Preferences plugin (Capacitor documentation)
- App plugin (Capacitor documentation)
- Keyboard plugin (Capacitor documentation)
- System Bars plugin (Capacitor documentation)
- Capacitor HTTP (Capacitor documentation)
- Capacitor Cookies (Capacitor documentation)
- Splash Screens and Icons (Capacitor documentation)
- Requesting access to protected resources (Apple Developer)
- Build Options, build.outDir (Vite documentation)
- Server Options, server.host (Vite documentation)
- Different History modes (Vue Router documentation)
- Deployment, Static Hosting (Nuxt 4.x documentation)
- Rendering Modes (Nuxt 4.x documentation)
- App Review Guidelines, Guidelines 4.2 and 5.1.1 (Apple Developer)
- Upcoming requirements for App Store Connect (Apple Developer)
- Target API level requirements for Google Play (Android Developers)
