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.js22 or later
Xcode (iOS)26.0 or later, so a Mac
Android Studio2025.2.1 (Otter) or later
Minimum iOS targetiOS 15.0
AndroidminSdkVersion 24, targetSdkVersion 36
iOS dependenciesSwift 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.

NeedPluginWatch out for
Photos, gallery@capacitor/cameraPermission strings in Info.plist
Push notifications@capacitor/push-notificationsPush capability on iOS, google-services.json on Android
Key-value storage@capacitor/preferencesNot a database, not encrypted
Links, back button@capacitor/appAndroid back button to handle
Keyboard@capacitor/keyboardResize 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?
Yes. Capacitor packages the build of a Vue.js app inside a real iOS and Android app that can be published on the App Store and Google Play. The Vue code stays as it is, and device features (camera, notifications, storage) are available through plugins called from JavaScript.
Does Capacitor work with Nuxt?
Yes, as long as Nuxt produces a static build. Generate the app with nuxt generate, ideally with ssr: false, and set .output/public as Capacitor's webDir. Nuxt server routes are not part of that build and must be hosted elsewhere.
Which webDir should I use for Capacitor with Vue and Vite?
For a Vue 3 project built with Vite, Capacitor's webDir is dist, Vite's default output folder. For Nuxt with static generation, it is .output/public. That folder must contain the final index.html file.
Do I need to switch Vue Router to hash mode for Capacitor?
Usually not. Vue Router's history mode (createWebHistory) works in a Capacitor app, and the Vue example in Capacitor's deep linking documentation calls router.push with a regular path. Hash mode remains a fallback.
Which Node and Xcode versions does Capacitor need in October 2026?
As of October 2026, Capacitor 8, the stable version, requires Node.js 22 or later, Xcode 26.0 or later and Android Studio 2025.2.1 or later, with a minimum target of iOS 15.0 and Android API 24. Ionic forecasts Capacitor 9 for the end of November 2026.
Can Apple reject a Vue app wrapped with Capacitor?
Yes, if it merely reproduces the website: Apple can then reject it under Guideline 4.2 "Minimum Functionality". Capacitor itself is not the target; the app has to offer features and usability specific to mobile.

Need help with this?

Website → Mobile App (Capacitor)

From €8,500 excl. VAT3 to 6 weeks

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 →

More articles

My Lovable app is getting slow or unstable: when should I move off it?

Lovable app slow, bugs with every change, security doubts: the real causes, what you can fix without leaving Lovable, and when to hand the code to a developer.

Read more

Lovable app rejected by Apple: the 5 most common reasons (and how to fix them)

Lovable app rejected by the App Store? The 5 most common Apple rejection reasons (4.2, 2.1, 5.1.1(v), 4.8, 3.1.1), how to fix each one and resubmit.

Read more

Tell us about your project

Our office

  • France
    1 RUE DE STOCKHOLM
    75008, PARIS