Skip to content
Release notesMultiplatform

Capacitor 8: Swift Package Manager, Android 16 and the upgrade

Capacitor 8.0.0 was published on 8 December. New iOS projects get Swift packages instead of CocoaPods, Android apps target Android 16 and draw under the system bars, and one command does most of the migration.

vuejs.school team9 Dec 20258 min readCapacitor 8.0

Capacitor is the bridge that turns a built Vue app into an iOS and Android app: your dist folder goes into a native shell, and plugins give it access to the device. Version 8.0.0 was published on 8 December, after an alpha in August and a beta in November, with an announcement and an 8.0 updating guide.

If you ship a Capacitor 7 app, two changes matter most. New iOS projects use Swift Package Manager instead of CocoaPods, and you choose when an existing app follows. On Android 16 your page runs under the status and navigation bars, and your layout has to make room for them. The rest is higher minimums, a few plugin behaviour changes and one migration command, shown below on the Vite and Vue app we wrapped with Capacitor 7 in September.

What shipped

The release has two headline features and a long list of raised requirements:

  • New iOS projects use Swift Package Manager (SPM) by default. CocoaPods projects keep working, and npx cap add ios --packagemanager CocoaPods still creates one.
  • A System Bars plugin, bundled with @capacitor/core, replaces the old android.adjustMarginsForEdgeToEdge option and handles edge-to-edge layouts on Android with CSS variables for the safe areas.
  • Minimums move to Node 22, Xcode 26, iOS 15, Android Studio Otter, minSdkVersion 24, and compileSdkVersion and targetSdkVersion 36 (Android 16).
  • Every official plugin moves to 8.0.0, with behaviour changes in Geolocation, Screen Orientation, Barcode Scanner and Status Bar.
  • Smaller items from the changelog: npx cap run can pick a device by target name, iOS plugins can hook into WebView authentication challenges, and bridge_layout_main.xml is renamed capacitor_bridge_layout_main.xml.

Swift Package Manager becomes the iOS default

CocoaPods has been the way iOS projects pull in dependencies since 2011, and it is winding down: it entered maintenance mode in August 2024, and its package index becomes read-only on 2 December 2026. SPM is Apple's own tool, built into Xcode. Capacitor has offered it as an option since version 6; in 8 it becomes the default for projects created with npx cap add ios. The team's October post on SPM explains the plan: Ionic keeps publishing CocoaPods versions of its libraries until that December 2026 date. The SPM guide adds that almost all plugins in the official capacitor-plugins repository already support SPM.

An existing CocoaPods project stays on CocoaPods after the upgrade. The updating guide's only CocoaPods step is to raise platform :ios in ios/App/Podfile to 15.0. You have a year to move, but check two limits first:

  • You cannot mix SPM and CocoaPods in one project, so a single third-party plugin without SPM support keeps the whole app on CocoaPods. The migration assistant and npx cap sync warn about each one.
  • A plugin that mixes Objective-C and Swift cannot be converted automatically, because an SPM target holds one language.

There are two ways to move. If nothing in the ios folder was ever edited by hand, delete the folder and recreate it: npx cap add ios now gives you the SPM template. Otherwise, use the migration assistant. The September app needs it, because recreating the folder would drop the camera permission strings it added to ios/App/App/Info.plist. Commit first, because the assistant deletes files:

npx cap spm-migration-assistant
npx cap open ios

The assistant runs pod deintegrate, deletes the Podfile, Podfile.lock and App.xcworkspace, creates a CapApp-SPM package that references all your plugins, generates its Package.swift, and adds a debug.xcconfig. Two steps remain in Xcode, and the SPM guide shows them with screenshots: add CapApp-SPM as a local package dependency, and select debug.xcconfig as a configuration file.

Handle Android edge-to-edge with System Bars

Android 15 started drawing the apps that target it under the status bar and the navigation bar (a layout Android calls edge-to-edge), and an app could opt out with the theme attribute windowOptOutEdgeToEdgeEnforcement. Capacitor 8 targets Android 16, and for apps targeting API level 36 Android's own behaviour changes say the opt-out is deprecated and disabled on Android 16 devices. Your web view will extend under the bars, so your layout has to know where they are.

Capacitor 7 tried to help with android.adjustMarginsForEdgeToEdge, which added margins around the web view. Capacitor 8 removes that option and moves the job into CSS, where it belongs.

The web view reports the bar heights through the standard env(safe-area-inset-*) values. Some Android WebView versions before 140 report them wrongly, so the System Bars plugin also injects corrected values into --safe-area-inset-* variables. The documented pattern goes in your global stylesheet and reads the variable first, with the env() value as a fallback:

src/assets/main.css
html {
  padding-top: var(--safe-area-inset-top, env(safe-area-inset-top, 0px));
  padding-bottom: var(--safe-area-inset-bottom, env(safe-area-inset-bottom, 0px));
  padding-left: var(--safe-area-inset-left, env(safe-area-inset-left, 0px));
  padding-right: var(--safe-area-inset-right, env(safe-area-inset-right, 0px));
}

On iOS the same env() values only work when the viewport meta tag in index.html includes viewport-fit=cover. The index.html that create-vue generates does not have it, so extend its viewport line to <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">.

The plugin also styles the bars. It is part of @capacitor/core, so there is nothing to install, and a dark mode toggle in a Vue component can keep the bar icons readable:

src/components/ThemeToggle.vue
<script setup lang="ts">
import { ref } from 'vue'
import { Capacitor, SystemBars, SystemBarsStyle } from '@capacitor/core'

const dark = ref(false)

async function toggle() {
  dark.value = !dark.value
  document.documentElement.classList.toggle('dark', dark.value)
  if (Capacitor.isNativePlatform()) {
    await SystemBars.setStyle({ style: dark.value ? SystemBarsStyle.Dark : SystemBarsStyle.Light })
  }
}
</script>

<template>
  <button type="button" @click="toggle">{{ dark ? 'Light theme' : 'Dark theme' }}</button>
</template>

SystemBarsStyle.Dark means light icons on a dark background, the same convention as the Status Bar plugin. isNativePlatform() skips the call in a browser, which has no system bars. The rest of the plugin, from the System Bars docs:

  • show() and hide(), which take a bar option to act on one bar only, such as the navigation bar.
  • setAnimation(), on iOS only.
  • Settings in capacitor.config.ts under plugins.SystemBars: the initial style, a hidden flag and the Android insetsHandling mode (css by default, disable to turn the injection off).
  • On iOS the plugin needs UIViewControllerBasedStatusBarAppearance set to YES in Info.plist.

The new minimums

Check these before you upgrade:

  • Node 22 or newer. The CLI checks its engines field and refuses to run on older versions; Node 24 is the current long-term support line.
  • Xcode 26, which itself requires macOS 15.6 or later.
  • An iOS deployment target of 15.0 for the project and every target.
  • Android Studio Otter (2025.2.1) or newer, with the Android Gradle Plugin at 8.13.0, the Gradle wrapper at 8.14.3 and, if you use Kotlin, kotlin_version 2.2.20.
  • In variables.gradle: minSdkVersion 24, compileSdkVersion 36 and targetSdkVersion 36, plus the AndroidX versions the guide lists.

Upgrade the Vue app from September

The app from the Capacitor 7 post is a Vite project that builds into dist, with android and ios folders (iOS still on CocoaPods, Capacitor 7's default) and one plugin, Camera. Start on a branch with Node 22 or 24 active, then let the CLI do the work:

npm install -D @capacitor/cli@latest
npx cap migrate

npx cap migrate moves @capacitor/core, @capacitor/android, @capacitor/ios and the official plugins to 8.0.0, applies the Gradle and Xcode changes it can, and prints anything it could not do. For those, the guide lists the manual steps. On Android:

  • Run the AGP Upgrade Assistant from the Tools menu in Android Studio and pick 8.13.0.
  • In app/build.gradle, change namespace "..." and compileSdk ... to the namespace = "..." form.
  • Update google-services to 4.4.4.
  • Add density to the activity's configChanges in AndroidManifest.xml, so that the web view no longer reloads when the window is resized.

On iOS, set the deployment target to 15.0 in the project's build settings and in ios/App/Podfile.

Before you run the app, add the safe-area CSS and the viewport-fit=cover change from the edge-to-edge section, or the first thing you will see on Android 16 is your header under the status bar. Then rebuild the web app and push it into both shells:

npm run build
npx cap sync
npx cap run ios
npx cap run android

Behaviour changes to check before you ship

Three changes can alter how an app that already works behaves:

  • Geolocation's timeout now applies to every request on Android and iOS, not only on the web and to getCurrentPosition() on Android. An app that set a short timeout may start seeing timeouts; the guide suggests a higher value. watchPosition() on Android gains an interval option.
  • Screen Orientation's lock() and Barcode Scanner's scanOrientation have no effect on tablets and other large screens running Android 16, because the system ignores orientation requests on displays whose smallest width is 600dp or more. Regular phones are unaffected. The manifest property android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY opts out temporarily and stops working at API level 37.
  • appendUserAgent on iOS used to gain two extra spaces through a bug. The bug is fixed, so a server that matched on the old string sees a different one. To keep the old string, add a space to ios.appendUserAgent, not to the root appendUserAgent, which would change Android too.

What to test on each platform

On iOS, build once in Xcode 26 and watch the build log for deprecated settings, then check the status bar in both themes and the safe areas on a device with a notch, in portrait and in landscape. If you migrated to SPM, test a clean clone: delete DerivedData, run npx cap sync, open the project and build, which proves the generated Package.swift is complete.

On Android, test on an Android 16 device or emulator and on an Android 14 one, because the bars behave differently under each. Scroll a long page to the bottom and confirm the last item clears the navigation bar; open the keyboard in a form and confirm the input stays visible; rotate a tablet; and if the app locks orientation, see what the lock now does on a large screen.

Upgrade now or wait

Upgrade now if your app is in development or you can test both platforms this week. The requirements are the real work, and they are coming anyway: Google Play will ask for a current target SDK, Apple will ask for a current Xcode, and CocoaPods has a date on it. The migration command handles the mechanical part.

Wait if you depend on a third-party plugin that has not published an 8.0 version yet, or if your CI builds on an older macOS image: Xcode 26 needs macOS 15.6, and a build agent that cannot provide it cannot ship Capacitor 8.

Check it stuck3 questions from the Mobile apps with Capacitor quiz.
Practice now
Was this helpful?

Keep reading