---
title: "Capacitor + Angular: Turn Your Web App into iOS & Android"
description: Add Capacitor to an existing Angular app step by step — set webDir, run on iOS and Android, and wire up Firebase, the camera and file uploads on device.
date:
  created: 2026-04-27
  updated: 2026-09-29
authors:
  - djabif
categories:
  - Capacitor
  - Firebase
  - Guides
links:
  - Capacitor File Picker: sdks/capacitor/file-picker.md
  - Capacitor File Compressor: sdks/capacitor/file-compressor.md
  - Capacitor Firebase Cloud Firestore: sdks/capacitor/firebase/cloud-firestore.md
  - Capacitor Firebase Cloud Storage: sdks/capacitor/firebase/cloud-storage.md
search:
  boost: 2
faq: true
---

# Capacitor + Angular: Turn Your Web App into iOS & Android

You can turn an existing Angular web app into native iOS and Android apps with Capacitor without rewriting it. This guide walks through the full Capacitor + Angular setup on a real demo app called Trip Expenses, built with Angular 21, Ionic 8, Capacitor 8 and Firebase, from the first command to camera uploads and file attachments on a real phone. It assumes you know Angular and have never built a mobile app.

<!-- more -->

???+ tip "Get the code"

    The complete source code is available on [GitHub](https://github.com/capawesome-team/capacitor-angular-starter-guide){:target="_blank"}, free to clone and adapt for your own project.

**Key takeaways:**

- You don't rewrite anything: the same Angular code runs on the web, iOS and Android. Capacitor changes how the app is packaged and what it can access, not how you write it.
- Get the empty shell running on a real device before adding a single plugin. Most first-run failures are a configuration mismatch, not a bug in your Angular code.
- Ionic is optional. If your app already has a design system, add Capacitor to plain Angular and keep it.
- The biggest change is the release process, not the code: web deploys become native builds and store reviews, unless a change is web-only and ships as a Live Update.
- On a phone, use the native Firebase plugins instead of the JavaScript SDK: they're more reliable inside the app and faster with large files, and the demo switches between them automatically.

The demo covers the parts most production apps need: trip and expense management, receipt attachments as images or PDFs, image compression before upload, profile pictures taken with the phone camera, and custom app icons and splash screens (the image shown while the app opens) for both platforms. The stack is Angular 21 with standalone components, Ionic 8 for the UI, Capacitor 8 to turn it into a phone app, and Firebase (Cloud Firestore for data, Cloud Storage for files).

On the Capacitor side, we rely on the [Camera](https://capacitorjs.com/docs/apis/camera){:target="_blank"} and [Splash Screen](https://capacitorjs.com/docs/apis/splash-screen){:target="_blank"} plugins from the Capacitor team, together with the [Capacitor File Picker plugin](../../sdks/capacitor/file-picker.md), [Capacitor File Compressor plugin](../../sdks/capacitor/file-compressor.md), [Capacitor Firebase Cloud Firestore plugin](../../sdks/capacitor/firebase/cloud-firestore.md), and [Capacitor Firebase Cloud Storage plugin](../../sdks/capacitor/firebase/cloud-storage.md) from Capawesome. For the full capture-to-storage pipeline behind that profile picture flow, see [How to Take and Edit Photos in a Capacitor App](./how-to-take-and-edit-photos-in-a-capacitor-app.md).

???+ note

    The example uses Angular, but the same concepts apply to any web project. Capacitor is framework-agnostic; the [Capacitor Setup Guide for Any Web Framework](./build-mobile-apps-with-any-web-framework-and-capacitor.md) covers React, Vue and Svelte.

## Capacitor Concepts for Angular Developers

Before jumping into the code, let's clarify a few concepts, especially if this is your first mobile app.

### What Capacitor Does

Capacitor is a free, open-source tool from the Ionic team that puts your Angular production build inside a real iOS or Android app. Think of it as a native app whose only screen is a browser window showing your app, without the address bar. Your Angular code still powers the UI and business logic, so you keep one codebase. When you need something a website can't do (open the camera, read a file from the phone), you add a plugin for it. Releases then go through the App Store and Google Play like any other app.

### Angular Web vs. Native Runtime

An Angular app inside Capacitor runs in a `WebView`, the browser component built into iOS and Android, rather than in a browser tab. Three things change. First, permissions: the phone asks the user before your app can use the camera or the photo library, and you have to declare each permission in the native project (`Info.plist` on iOS, `AndroidManifest.xml` on Android). Second, files: instead of `Blob`s in memory you often get a path to a file on the phone, and phones have far less memory than a laptop, so a 20 MB PDF handled the "web way" can crash the app. Third, releases: a deploy is no longer a copy to a server but a new app version that goes through store review.

Your Angular code still drives most of the product; how well you handle those three differences decides how reliable the app feels.

### What a Capacitor Plugin Is

A Capacitor plugin is an npm package that gives your TypeScript code a function like `Camera.getPhoto()` and does the real work on the phone in Swift or Kotlin. You never touch that native code. Most plugins also include a browser version, so the same call keeps working when you run `ng serve`. In this project, plugins handle the camera, file picking and Firebase on the phone, while everything else stays plain Angular.

## Add Capacitor to Your Angular Project

Install Capacitor (the runtime and its command-line tool) and set it up in your Angular project:

```bash
npm install @capacitor/core@latest
npm install -D @capacitor/cli@latest
npx cap init
```

The command asks three questions: your app name (e.g. `Trip Expenses`), an app ID (e.g. `io.tripexpenses.app`, a unique identifier written like a reversed domain name that the stores use to tell your app apart from every other app, and that you can't change after publishing), and the folder where `ng build` writes its output (`www` or your Angular `dist/` folder). Use your own values here.

Next, add the iOS and Android platforms. Each command creates a folder with a complete native project inside:

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

You now have an `ios/` and an `android/` folder next to `src/`. They are real Xcode and Android Studio projects that display your Angular build, and you rarely need to open them.

### Point webDir at the Angular Build

The `webDir` value in `capacitor.config.ts` tells Capacitor which folder to copy into the phone app, so it has to match the folder where `ng build` writes its output, defined by `outputPath` in `angular.json`. The Trip Expenses demo uses `www`, the convention Ionic Angular starters follow:

```json title="angular.json"
"build": {
  "builder": "@angular-devkit/build-angular:browser",
  "options": {
    "outputPath": "www",
    "index": "src/index.html",
    "main": "src/main.ts"
  }
}
```

```typescript title="capacitor.config.ts"
import type { CapacitorConfig } from '@capacitor/cli';

const config: CapacitorConfig = {
  appId: 'io.tripexpenses.app',
  appName: 'Trip Expenses',
  webDir: 'www',
  server: {
    androidScheme: 'https'
  },
  plugins: {
    SplashScreen: {
      launchAutoHide: false,
      showSpinner: false
    }
  }
};

export default config;
```

A project created with `ng new` on Angular 17 or later writes to `dist/<project-name>/browser` instead, so set `webDir` to that path unless you change `outputPath`. Every time you run `npx cap sync`, Capacitor copies the `webDir` folder into the `ios/` and `android/` projects. If `webDir` points at a folder without an `index.html`, the app opens to an empty white page. That blank screen is the most common first-run problem when adding Capacitor to an Angular project, and it is almost never a code bug.

The demo's `npm run build` script runs `ng build` with the production configuration, so the files Capacitor copies are always the optimized bundle, not the development one.

### Build, Sync, and Run

Unlike `ng serve`, the phone doesn't pick up your changes automatically. Whenever you change Angular code, rebuild and copy the result into the native projects:

```bash
npm run build
npx cap sync
```

To open the native projects in Xcode (Mac only) or Android Studio, for example to run the app on a simulator or a connected phone:

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

Or skip those tools and run straight on a simulator or a phone plugged into your computer:

```bash
npx cap run ios
npx cap run android
```

### Generate App Icons and Splash Screens

iOS and Android each need the app icon and splash screen in a dozen sizes. The [@capacitor/assets](https://github.com/ionic-team/capacitor-assets){:target="_blank"} tool generates all of them from two images. Install it, drop your own `icon-only.png` and `splash.png` into a `resources/` folder, and run:

```bash
npm install -D @capacitor/assets@latest
npx capacitor-assets generate --ios
npx capacitor-assets generate --android
```

This writes every required size into the `ios/` and `android/` projects. The tool has more options (background colors, dark mode variants) in its README.

## Set Up Your Firebase Project

If you clone the [demo app](https://github.com/capawesome-team/capacitor-angular-starter-guide){:target="_blank"}, you'll need to create your own Firebase project before running it.

### 1) Create Firebase Resources

In the [Firebase Console](https://console.firebase.google.com/){:target="_blank"}, create a new project and enable both **Cloud Firestore** and **Cloud Storage**.

### 2) Register Your App Clients

Under the same Firebase project, register three "apps" (Firebase's word for each platform that connects to it): a Web app for the browser version, an Android app with package name `io.tripexpenses.app`, and an iOS app with bundle ID `io.tripexpenses.app`. Package name and bundle ID are the Android and iOS names for the app ID you chose in `npx cap init`; replace them with your own.

### 3) Configure Firebase in Angular Environments

Copy the Web app config Firebase shows you into `src/environments/environment.ts` and `src/environments/environment.prod.ts`, the same way you would for any Angular app. Angular's `fileReplacements` swap the first file for the second in production builds, so the Firebase project your store build talks to is decided when you build, and you can point development at a test project.

### 4) Add Native Firebase Config Files

The native Firebase plugins don't read `environment.ts`; they read a config file inside each native project. When you registered the Android and iOS apps in step 2, Firebase offered those files for download. Place `google-services.json` at `android/app/google-services.json` and `GoogleService-Info.plist` at `ios/App/App/GoogleService-Info.plist`, then run:

```bash
npx cap sync
```

Both files are excluded from Git in the demo repository, so every developer drops in the files for their own Firebase project.

## Integrate Firebase with Capacitor

With your Firebase project ready, the next step is wiring it into the app. You could keep using the Firebase JavaScript SDK you already know, and it does run inside the app. We use the native Firebase plugins instead: on a phone they are more reliable (the JavaScript SDK was written for browsers, not for apps that get suspended in the background), they can upload a file straight from the phone's storage without loading it into memory first, and they are noticeably faster with large files.

To install them, see the [Installation](../../sdks/capacitor/firebase/cloud-firestore.md#installation) section of the Firebase Cloud Firestore plugin and the [Installation](../../sdks/capacitor/firebase/cloud-storage.md#installation) section of the Firebase Cloud Storage plugin.

## Add Native Features to Angular

The demo app includes two features that need the phone itself, so you can see what adding a plugin looks like in an Angular codebase.

### Profile Picture Upload with Camera

On the profile screen, users can update their profile picture by taking a photo or picking one from the gallery. The Angular UI stays exactly the same. The [Camera](https://capacitorjs.com/docs/apis/camera){:target="_blank"} plugin from the Capacitor team opens the phone's camera or photo library and hands your code the picture. In the browser, the same call opens a small web camera dialog instead, so you can keep developing with `ng serve`. That browser version comes from the `@ionic/pwa-elements` package, which the demo registers once in `src/main.ts` with `defineCustomElements(window)` before bootstrapping the Angular app.

<figure markdown>
  ![Capacitor Camera Plugin Example](../../assets/images/screenshots/capacitor-camera-plugin.png){ width="300" }
  <figcaption>Upload Profile Picture</figcaption>
</figure>

### Expense Attachments with File Picker

When creating an expense, users can attach a receipt image or PDF. Three plugins work together for this flow: the [Capacitor File Picker plugin](../../sdks/capacitor/file-picker.md) opens the phone's file chooser, the [Capacitor File Compressor plugin](../../sdks/capacitor/file-compressor.md) shrinks photos to at most 1600 pixels on the long side so a 5 MB receipt becomes a few hundred KB, and the [Capacitor Firebase Cloud Storage plugin](../../sdks/capacitor/firebase/cloud-storage.md) uploads the result. All of the related code lives in one Angular service, `src/app/core/services/storage.service.ts`.

That service is also where the app decides between the phone and the browser. `Capacitor.isNativePlatform()` returns `true` on a phone, and the service then gives the Cloud Storage plugin the file's location on the device, so the file is uploaded from storage without ever being loaded into memory. In the browser, it falls back to `uploadBytes()` from the Firebase JavaScript SDK with a `Blob`. One Angular service, two upload paths, and the components calling it don't know the difference.

<figure markdown>
  ![Capacitor File Picker Example](../../assets/images/screenshots/capacitor-file-picker.png){ width="300" }
  <figcaption>File Picker</figcaption>
</figure>

For a deeper look at file handling on a phone, including how to avoid running out of memory, see [Capacitor File Handling: The Complete Guide](./capacitor-file-handling-guide.md). For everything the Cloud Storage plugin itself can do (upload progress, security rules, pagination), see [Upload & Manage Files with Firebase Storage in Capacitor](./capacitor-firebase-cloud-storage-guide.md).

### Plugin Events and NgZone

Some plugins notify you of events, for example when the user sends the app to the background. Those callbacks come from native code, outside Angular's zone, so a property you set inside one won't show up in the template until something else triggers change detection. Wrap the state change in `NgZone.run()`, exactly as you would for a third-party library that calls you back outside Angular. The demo uses `provideZoneChangeDetection()`, so this applies to it as well as to any NgModule-based app. Here is how the demo reacts to the app moving to the background with the `@capacitor/app` plugin:

```typescript
import { Component, NgZone, inject } from '@angular/core';
import { App } from '@capacitor/app';

@Component({ /* ... */ })
export class ProfilePage {
  private readonly ngZone = inject(NgZone);
  isActive = true;

  constructor() {
    App.addListener('appStateChange', ({ isActive }) => {
      this.ngZone.run(() => (this.isActive = isActive));
    });
  }
}
```

Plugin methods you `await` directly (like `Camera.getPhoto()` or `FilePicker.pickFiles()`) don't need this treatment, because the promise resolves inside the zone that started it, and the template updates as usual.

That covers everything needed to bring your Angular app to a phone. The rest is Angular and Ionic UI to build out your product.

## Production Delivery with Capawesome Cloud

Once the app runs reliably on real phones, the next step is getting it to users, and that is where mobile differs most from the web. An iOS app can only be built on a Mac, both stores require the app to be signed with certificates you have to create and renew, and every release waits for store review. Capawesome Cloud handles those parts: [Native Builds](../../cloud/native-builds/setup.md) builds and signs the iOS and Android apps in the cloud, so nobody on the team needs a Mac, [Live Updates](../../cloud/live-updates/setup.md) pushes changes to your Angular code to installed apps without a new store release, and [App Store Publishing](../../cloud/app-store-publishing/index.md) uploads builds straight to TestFlight (Apple's beta testing service) or Google Play.

A pattern that works well in practice is combining the two release tracks. Ship a new store version when you add or update a plugin, and use Live Updates for everything that is only Angular code in between. Wire it all into your repo with the [GitHub integration](../../cloud/integrations/github.md) and your pipeline triggers builds and deployments automatically on commit, branch, or tag.

<div class="capawesome-z29o10a">
  <a href="https://capawesome.io/" target="_blank">
    <img alt="Thousands of teams ship faster with Capawesome Cloud Native Builds and Live Updates" src="https://capawesome.io/assets/banners/cloud-teams-ship-faster-with-capacitor.png" />
  </a>
</div>

See the [Capawesome Cloud documentation](../../cloud/index.md){:target="_blank"} for setup details, or browse the [video walkthroughs](https://www.youtube.com/@capawesomeio){:target="_blank"} for each step.

## FAQ

### Do I need Ionic to wrap an Angular app with Capacitor?

No. Ionic is entirely optional. Capacitor only turns your Angular build into a phone app; Ionic is a separate library of mobile-style UI components (tabs, lists, modals) that you can add on top. If your Angular app already has its own design system, add Capacitor directly and skip Ionic.

### Which webDir should I use for an Angular app?

Use whatever `outputPath` in `angular.json` says. Ionic Angular projects set it to `www`; a plain Angular CLI project created on Angular 17 or later writes to `dist/<project-name>/browser`. If the two values disagree, `npx cap sync` copies the wrong folder and the app opens to a blank screen.

### Does Capacitor work with the latest Angular version?

Yes. Capacitor only consumes the built output, so it has no dependency on the Angular version, the standalone-versus-NgModule choice, or zone.js. The demo runs Angular 21 with standalone components and `provideZoneChangeDetection()`, but an older NgModule app builds and syncs the same way.

### How do I turn an Angular app into an Android app?

Run `npm install @capacitor/android`, then `npx cap add android`, `npm run build` and `npx cap sync`. That creates a Gradle project in the `android/` folder which you can open in Android Studio with `npx cap open android` and run on an emulator or device. The same steps with `ios` produce an Xcode project, which requires a Mac to build locally or a cloud build service such as [Capawesome Cloud Native Builds](../../cloud/native-builds/setup.md).

### Why use native Firebase plugins instead of the Firebase Web SDK, since the app already runs Angular?

Because the Firebase JavaScript SDK was written for browser tabs, not for apps that get suspended in the background. The native plugins behave more predictably on a phone, can upload a file straight from the phone's storage without loading it into memory, and are noticeably faster with large files. The trade-off is installing one plugin per Firebase product instead of a single JavaScript package.

### After wrapping my app, do web deploys still work the same way?

No. Releases now go through the native app store workflow instead of a simple web deploy. Your Angular code still powers the UI, but shipping a change to users means either a native build submission (for native-layer changes) or a Live Update (for web-layer-only changes), rather than deploying updated static assets to a web server.

### Does wrapping the app change how file paths work in my Angular code?

Yes. On a phone, a picked file usually arrives as a location on the device (a path starting with `file://` or `content://`) instead of a `Blob` in memory, which web-only Angular code never has to handle. Phones also have far less memory than a laptop, so file-heavy flows like the receipt attachments in this guide need to work with those paths instead of loading whole files into JavaScript.

### Can I add native plugins gradually, or do I have to wire up every capability before shipping?

Gradually. That's the intended workflow. Since your Angular code keeps driving the UI and business logic, you can ship an initial wrapped version with minimal native touchpoints and add plugins (camera, file picker, push notifications, biometrics) over time as you need them, rather than front-loading every native integration before your first release.

## Related Posts

- [Capacitor Firestore: Real-Time Data & Offline Sync](./capacitor-firebase-cloud-firestore-guide.md)
- [What's New in Capacitor Firebase 8.3.0](./capacitor-firebase-8-3-0-release.md)

## Try Capawesome Cloud

Stop fighting with manual builds and slow release cycles. Capawesome Cloud handles native builds, live updates, and app store publishing for your Capacitor apps in one place.

[Try Capawesome Cloud Free](https://capawesome.io){ .md-button .md-button--primary }

## Conclusion

Wrap the app before you add plugins. Get the Angular build running on a real iOS and Android device with nothing but the Capacitor core and platform packages, then add one native integration at a time so each one can be debugged on its own.

Which plugin comes next depends on your product: [push notifications](./capacitor-push-notifications-guide.md), [in-app purchases](./tips-for-setting-up-in-app-purchases-with-capacitor.md), or authentication with [OAuth](../../sdks/capacitor/oauth.md) and [biometrics](./exploring-the-capacitor-biometrics-api.md).

For a deeper look at how to roll out updates safely once you're in production, see [Capacitor App Updates: The Complete Guide](./capacitor-app-update-guide.md).

Getting the app into the stores involves a few more steps. For a complete walkthrough covering developer accounts, certificates, builds, store listing, and Live Updates setup, see [From Web App to App Store: The Complete Guide](./11-steps-to-get-your-web-app-on-the-app-store.md). If you're coming from an AI app builder rather than a hand-written codebase, the beginner-friendly [Convert Your Lovable App to iOS & Android Apps](./convert-lovable-app-to-mobile-app.md) follows the same path end to end.

If you have questions, join the [Capawesome Discord server](https://discord.gg/VCXxSVjefW){:target="_blank"} and connect with the community. To stay up to date on the latest news, subscribe to the [Capawesome newsletter](https://capawesome.io/newsletter/){:target="_blank"}.
