---
title: "Turn a Vue app into a mobile app with Capacitor 7 · vuejs.school"
description: "Wrap a Vite-built Vue app in Capacitor 7, run it on an iPhone and an Android device, use the camera with a web fallback, and get live reload working."
canonical_url: "https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7"
meta:
  "og:description": "Wrap a Vite-built Vue app in Capacitor 7, run it on an iPhone and an Android device, use the camera with a web fallback, and get live reload working."
  "og:image": "https://vuejs.school/_og/d/a_~VHVybiBhIFZ1ZSBhcHAgaW50byBhIG1vYmlsZSBhcHAgd2l0aCBDYXBhY2l0b3IgNyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Turn%2Ba%2BVue%2Bapp%2Binto%2Ba%2Bmobile%2Bapp%2Bwith%2BCapacitor%2B7%2Cdescription_~V3JhcCBhIFZpdGUtYnVpbHQgVnVlIGFwcCBpbiBDYXBhY2l0b3IgNywgcnVuIGl0IG9uIGFuIGlQaG9uZSBhbmQgYW4gQW5kcm9pZCBkZXZpY2UsIHVzZSB0aGUgY2FtZXJhIHdpdGggYSB3ZWIgZmFsbGJhY2ssIGFuZCBnZXQgbGl2ZSByZWxvYWQgd29ya2luZy4%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy90dXJuLWEtdnVlLWFwcC1pbnRvLWEtbW9iaWxlLWFwcC13aXRoLWNhcGFjaXRvci03Ig%2Cs_Nh7v5PxvPqN_Qk4H.png"
  "og:image:alt": "Turn a Vue app into a mobile app with Capacitor 7 · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Turn a Vue app into a mobile app with Capacitor 7 · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~VHVybiBhIFZ1ZSBhcHAgaW50byBhIG1vYmlsZSBhcHAgd2l0aCBDYXBhY2l0b3IgNyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Turn%2Ba%2BVue%2Bapp%2Binto%2Ba%2Bmobile%2Bapp%2Bwith%2BCapacitor%2B7%2Cdescription_~V3JhcCBhIFZpdGUtYnVpbHQgVnVlIGFwcCBpbiBDYXBhY2l0b3IgNywgcnVuIGl0IG9uIGFuIGlQaG9uZSBhbmQgYW4gQW5kcm9pZCBkZXZpY2UsIHVzZSB0aGUgY2FtZXJhIHdpdGggYSB3ZWIgZmFsbGJhY2ssIGFuZCBnZXQgbGl2ZSByZWxvYWQgd29ya2luZy4%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy90dXJuLWEtdnVlLWFwcC1pbnRvLWEtbW9iaWxlLWFwcC13aXRoLWNhcGFjaXRvci03Ig%2Cs_Nh7v5PxvPqN_Qk4H.png"
  "twitter:image:alt": "Turn a Vue app into a mobile app with Capacitor 7 · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## How Capacitor wraps a web app

A Capacitor app has three parts:

- Your web app, built to static files.
- A native project per platform, `ios/` and `android/`, which you own and can open in Xcode or Android Studio.
- A bridge: a web view in each native project that loads your files from the device and lets JavaScript call native code through plugins.

Nothing about your Vue code changes. Routing, Pinia, your components: all the same. What changes is where the files come from (the app bundle, not a server) and what APIs exist (`Camera.getPhoto()` next to `fetch`).

The trade-off to know going in: the UI is still HTML and CSS, rendered by WebKit on iOS and by the Android WebView, so it looks like your web app rather than a native control kit. For most apps that is exactly right.

## Requirements: Node, Xcode and Android Studio

The [environment setup page](https://capacitorjs.com/docs/v7/getting-started/environment-setup) for Capacitor 7 lists what you need, and the [7.0 upgrade guide](https://capacitorjs.com/docs/v7/updating/7-0) gives the version floors:

- Node.js 20 or newer. Vite 7 wants 20.19 or 22.12 and above, so install the current LTS and both are happy.
- For iOS: a Mac, Xcode 16.0 or newer with its command line tools ( `xcode-select --install`), and CocoaPods, which the page recommends installing through Homebrew. Apps target iOS 14 and up.
- For Android: Android Studio Ladybug (2024.2.1) or newer, which ships the JDK 21 that Capacitor 7 needs, and at least one Android SDK platform at API 23 or higher. Capacitor 7 projects compile against API 35.

You can build and run the Android app from any operating system. The iOS app needs the Mac; there is no way around Apple's toolchain.

## Install Capacitor and create the native projects

In the root of the Vue project:

```
npm install @capacitor/core
npm install -D @capacitor/cli
npx cap init "Habits" school.vuejs.habits --web-dir dist
```

The two arguments are the app's display name and its ID in reverse-domain form. The ID becomes the bundle identifier on iOS and the package name on Android, and it is painful to change later, so pick the real one now. `--web-dir dist` tells Capacitor where Vite writes the build. The command creates `capacitor.config.ts` (or `capacitor.config.json` if the project has no TypeScript installed):

*capacitor.config.ts*

```
import type { CapacitorConfig } from '@capacitor/cli'

const config: CapacitorConfig = {
  appId: 'school.vuejs.habits',
  appName: 'Habits',
  webDir: 'dist',
}

export default config
```

Then add the platforms you want. Each one is a package plus a generated native project:

```
npm install @capacitor/android @capacitor/ios
npx cap add android
npx cap add ios
```

You now have `android/` and `ios/` folders. Commit them. They are source, not build output: you will edit them when a plugin needs a permission string, and the stores will need signing configuration in them.

One requirement on your `index.html`: it must have a `<head>` element, because the bridge injects its script there. Vite's template has one.

## Run the app on a simulator or a phone

The loop you will repeat is build, sync, run:

```
npm run build
npx cap sync
npx cap run ios
```

1. `npm run build` writes `dist/`.
2. `npx cap sync` copies `dist/` into both native projects and installs any native dependencies your plugins need (it runs `pod install` on the iOS side).
3. `npx cap run ios` builds the native app and deploys it to a target. With no target it asks you to choose from the simulators and connected devices; `npx cap run ios --list` prints them so you can pass `--target` next time.

`cap run` runs a sync of its own first (`--no-sync` skips it), so step 2 can be left out when you deploy with `cap run`. Keep it when you build from the IDE instead: `cap open`, below, copies nothing. Swap `ios` for `android` and the same commands drive an emulator or a phone with USB debugging on.

When you want the IDE, for signing, logs or a device that refuses to appear, open the project there instead:

```
npx cap open ios
npx cap open android
```

That opens the `.xcworkspace` in Xcode or the project in Android Studio, and you press Run. The first iOS run on a physical device will ask you to sign in with an Apple ID in Xcode and to trust the developer certificate on the phone; a free account is enough for development.

## Add a camera button with a web fallback

Plugins are npm packages with a native side. The [Camera plugin](https://capacitorjs.com/docs/v7/apis/camera) is the usual first one:

```
npm install @capacitor/camera
npx cap sync
```

iOS requires a reason string for each capability. Add three keys to `ios/App/App/Info.plist`: `NSCameraUsageDescription`, `NSPhotoLibraryUsageDescription` and `NSPhotoLibraryAddUsageDescription`, each with a sentence the user will see in the permission prompt. Android needs no extra permission for taking a photo unless you save it to the gallery.

The component below takes a photo on a device and picks one from the file system in a browser:

*src/components/PhotoButton.vue*

```
<script setup lang="ts">
import { ref } from 'vue'
import { Capacitor } from '@capacitor/core'
import { Camera, CameraResultType, CameraSource } from '@capacitor/camera'

const photoUrl = ref<string | null>(null)
const error = ref<string | null>(null)
const isNative = Capacitor.isNativePlatform()

async function takePhoto() {
  error.value = null
  try {
    const photo = await Camera.getPhoto({
      quality: 80,
      resultType: CameraResultType.Uri,
      source: isNative ? CameraSource.Camera : CameraSource.Photos,
    })
    photoUrl.value = photo.webPath ?? null
  } catch (e) {
    error.value = e instanceof Error ? e.message : 'Could not get a photo'
  }
}
</script>

<template>
  <button type="button" @click="takePhoto">
    {{ isNative ? 'Take a photo' : 'Choose a photo' }}
  </button>
  <img v-if="photoUrl" :src="photoUrl" alt="Your photo" />
  <p v-if="error" role="alert">{{ error }}</p>
</template>
```

What the lines do:

- `Capacitor.isNativePlatform()` is `true` inside the iOS or Android app and `false` in a browser. `Capacitor.getPlatform()` gives you `'ios'`, `'android'` or `'web'` when you need the distinction.
- On a device, `CameraSource.Camera` opens the system camera, and `resultType: CameraResultType.Uri` returns a `webPath` the web view can load straight into an `img`.
- In a browser, `CameraSource.Photos` opens a file picker and `webPath` is a `blob:` URL for the chosen file. `CameraSource.Camera` would first look for the camera modal from [PWA Elements](https://capacitorjs.com/docs/v7/web/pwa-elements), a separate package, and without it log an error and fall back to a file input. Choosing `Photos` in the browser skips the error and opens the picker straight away.
- The `catch` matters: cancelling the camera rejects the promise with a message rather than returning nothing, and your UI should not treat that as a crash.

Because the plugin ships this web implementation, the component also works under the normal `vite dev` server. Develop the layout in the browser, test the camera on the device.

## Use live reload on the device

Building and syncing after every change is slow. The [live reload guide](https://capacitorjs.com/docs/v7/guides/live-reload) shows the alternative: point the native app's web view at your Vite dev server instead of the bundled files. Start Vite so it listens on your network address, not just `localhost`:

```
npm run dev -- --host
```

Vite prints a `Network:` URL such as `http://192.168.1.20:5173`. Pass its address and port to `cap run`:

```
npx cap run android --live-reload --host 192.168.1.20 --port 5173
```

The phone must be on the same Wi-Fi as your computer. Now edits hot-reload inside the native app exactly as they do in the browser, and plugin calls still go to the real device. Keep the `--port` flag: without it the CLI assumes port 3000, not Vite's 5173.

The flags write a temporary `server.url` into the native project's copy of the config, and `cap run` restores that copy when you stop it with Ctrl+C. You could write the same thing into `capacitor.config.ts` as `server: { url: 'http://192.168.1.20:5173', cleartext: true }`, where `cleartext` allows plain HTTP. But then it is easy to commit by mistake, and a committed `server.url` ships an app that loads from a laptop that is not there. Keep it on the command line.

## Publish to the App Store and Google Play

A release build is the same `npm run build` and `npx cap sync`, followed by the platform's own release process, which the [iOS](https://capacitorjs.com/docs/v7/ios/deploying-to-app-store) and [Android](https://capacitorjs.com/docs/v7/android/deploying-to-google-play) deployment pages describe as no different from any other native app's:

- Icons and splash screens. The stores reject an app without a full icon set. The `@capacitor/assets` package generates every size for both platforms with `npx @capacitor/assets generate`, from source images in an `assets/` folder: an icon of at least 1024 by 1024 pixels and a splash image of at least 2732 by 2732.
- iOS: an Apple Developer Program membership, an archive built in Xcode with your distribution certificate, uploaded to App Store Connect, tested through TestFlight, then submitted for review.
- Android: a Google Play Console account and an upload key in a keystore file. Build a release bundle ( `.aab`) from Android Studio, or with `npx cap build android` and your keystore, and upload it to the Play Console. Google keeps the key that signs the app for users ([Play App Signing](https://developer.android.com/studio/publish/app-signing)), so a lost upload key can be reset.

Plan a day for the first submission on each store, most of it spent on forms, screenshots and privacy declarations rather than code. After that, each release is a build, a sync and an upload.

**Check it stuck**3 questions from the Mobile apps with Capacitor quiz.

[**Practice now**](https://vuejs.school/learn/multiplatform/mobile-apps-with-capacitor?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 7 min read**Tauri plugins, mobile builds and the updater**](https://vuejs.school/community/blog/tauri-plugins-mobile-builds-and-the-updater) [Tutorial · 7 min read**Mobile UI with Ionic Vue 8**](https://vuejs.school/community/blog/mobile-ui-with-ionic-vue-8)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-23","description":"Wrap a Vite-built Vue app in Capacitor 7, run it on an iPhone and an Android device, use the camera with a web fallback, and get live reload working.","name":"Turn a Vue app into a mobile app with Capacitor 7","url":"https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7","breadcrumb":{"@id":"https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-09-23","description":"Wrap a Vite-built Vue app in Capacitor 7, run it on an iPhone and an Android device, use the camera with a web fallback, and get live reload working.","headline":"Turn a Vue app into a mobile app with Capacitor 7","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~VHVybiBhIFZ1ZSBhcHAgaW50byBhIG1vYmlsZSBhcHAgd2l0aCBDYXBhY2l0b3IgNyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Turn%2Ba%2BVue%2Bapp%2Binto%2Ba%2Bmobile%2Bapp%2Bwith%2BCapacitor%2B7%2Cdescription_~V3JhcCBhIFZpdGUtYnVpbHQgVnVlIGFwcCBpbiBDYXBhY2l0b3IgNywgcnVuIGl0IG9uIGFuIGlQaG9uZSBhbmQgYW4gQW5kcm9pZCBkZXZpY2UsIHVzZSB0aGUgY2FtZXJhIHdpdGggYSB3ZWIgZmFsbGJhY2ssIGFuZCBnZXQgbGl2ZSByZWxvYWQgd29ya2luZy4%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy90dXJuLWEtdnVlLWFwcC1pbnRvLWEtbW9iaWxlLWFwcC13aXRoLWNhcGFjaXRvci03Ig%2Cs_Nh7v5PxvPqN_Qk4H.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/turn-a-vue-app-into-a-mobile-app-with-capacitor-7#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Turn a Vue app into a mobile app with Capacitor 7","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~VHVybiBhIFZ1ZSBhcHAgaW50byBhIG1vYmlsZSBhcHAgd2l0aCBDYXBhY2l0b3IgNyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Turn%2Ba%2BVue%2Bapp%2Binto%2Ba%2Bmobile%2Bapp%2Bwith%2BCapacitor%2B7%2Cdescription_~V3JhcCBhIFZpdGUtYnVpbHQgVnVlIGFwcCBpbiBDYXBhY2l0b3IgNywgcnVuIGl0IG9uIGFuIGlQaG9uZSBhbmQgYW4gQW5kcm9pZCBkZXZpY2UsIHVzZSB0aGUgY2FtZXJhIHdpdGggYSB3ZWIgZmFsbGJhY2ssIGFuZCBnZXQgbGl2ZSByZWxvYWQgd29ya2luZy4%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy90dXJuLWEtdnVlLWFwcC1pbnRvLWEtbW9iaWxlLWFwcC13aXRoLWNhcGFjaXRvci03Ig%2Cs_Nh7v5PxvPqN_Qk4H.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~VHVybiBhIFZ1ZSBhcHAgaW50byBhIG1vYmlsZSBhcHAgd2l0aCBDYXBhY2l0b3IgNyDCtyB2dWVqcy5zY2hvb2w%2Cc_OgCard.takumi%2Ctitle_Turn%2Ba%2BVue%2Bapp%2Binto%2Ba%2Bmobile%2Bapp%2Bwith%2BCapacitor%2B7%2Cdescription_~V3JhcCBhIFZpdGUtYnVpbHQgVnVlIGFwcCBpbiBDYXBhY2l0b3IgNywgcnVuIGl0IG9uIGFuIGlQaG9uZSBhbmQgYW4gQW5kcm9pZCBkZXZpY2UsIHVzZSB0aGUgY2FtZXJhIHdpdGggYSB3ZWIgZmFsbGJhY2ssIGFuZCBnZXQgbGl2ZSByZWxvYWQgd29ya2luZy4%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_lilac%2Cp_Ii9jb21tdW5pdHkvYmxvZy90dXJuLWEtdnVlLWFwcC1pbnRvLWEtbW9iaWxlLWFwcC13aXRoLWNhcGFjaXRvci03Ig%2Cs_Nh7v5PxvPqN_Qk4H.png"}]}
```