Cordova is in maintenance mode and Capacitor has been Ionic's official native runtime since 2019, but in 2026 we still meet plenty of Ionic apps running on Cordova. The migration is well documented by the Capacitor team; what teams lack is a checklist that tells them the order, and the places where the official guide assumes a cleaner project than the one they have. This is ours, updated for Capacitor 8.
0. Prerequisites
- Node 22 or later (Capacitor 8 requires it)
- Xcode 26 and a current Android Studio; check the Capacitor 8 upgrade guide for the exact minimums
- Your Ionic app builds and runs in a browser with
ionic serve— if it does not, fix that first; Capacitor cannot fix an app that does not build - A green test suite, or at least a smoke-test script you can run on device before and after
1. Plugin audit table
Every migration starts with a table. List every cordova-plugin-* in
package.json and config.xml, then decide its fate:
| Cordova plugin | Decision | Capacitor replacement |
|---|---|---|
| cordova-plugin-camera | Replace | @capacitor/camera |
| cordova-plugin-file | Replace | @capacitor/filesystem |
| cordova-plugin-splashscreen | Replace | @capacitor/splash-screen |
| cordova-plugin-statusbar | Replace | @capacitor/status-bar |
| cordova-plugin-device | Replace | @capacitor/device |
| cordova-plugin-network-information | Replace | @capacitor/network |
| cordova-plugin-inappbrowser | Replace | @capacitor/browser (for external URLs) |
| phonegap-plugin-push | Replace | @capacitor/push-notifications |
| cordova-plugin-whitelist | Remove | Not needed; Capacitor handles origins |
| cordova-plugin-ionic-webview | Remove | Built into Capacitor |
| cordova-plugin-ionic (Appflow) | Remove | See our Appflow migration guide |
| cordova-plugin-x-socialsharing | Replace | @capacitor/share |
| cordova-sqlite-storage | Replace | @capacitor-community/sqlite |
| Vendor SDK plugin with no equivalent | Keep (for now) | Load as a Cordova plugin under Capacitor, then rewrite |
Three columns matter: Replace with an official or Capawesome/community
plugin, Remove because Capacitor covers it, or Keep as a Cordova
plugin loaded by Capacitor. Capacitor runs most Cordova plugins unchanged, but
plugins that modify the Cordova config.xml at install time, depend on
cordova-plugin-ionic-webview, or hook into Cordova's build process will not
work and should be flagged as "Keep — verify on device" before you trust them.
2. Install Capacitor and create the config
npm install @capacitor/core @capacitor/cli@latest
npx cap init "My App" com.example.myapp --web-dir www
Use the same appId as the Cordova widget id; changing it creates a new
app in the stores. Then add the platforms:
npm install @capacitor/ios @capacitor/android
npx cap add ios
npx cap add android
Capacitor 8 creates the iOS project with Swift Package Manager by default.
If a kept Cordova plugin or a vendor SDK only ships CocoaPods, add iOS with
npx cap add ios --packagemanager CocoaPods instead.
3. config.xml → capacitor.config.ts mapping
Most of config.xml has no Capacitor equivalent because the native projects
are now yours to edit directly. The pieces that do map:
| config.xml | Capacitor |
|---|---|
<widget id> | appId in capacitor.config.ts |
<name> | appName |
<content src="index.html"> | webDir (the built output folder) |
<preference name="SplashScreenDelay"> | plugins.SplashScreen.launchShowDuration |
<preference name="StatusBarStyle"> | StatusBar.setStyle() at runtime |
<preference name="Orientation"> | Xcode / AndroidManifest.xml directly |
<allow-navigation href> | server.allowNavigation |
<access origin> | Usually unnecessary; CORS is a server concern |
<icon> / <splash> | @capacitor/assets generates native assets |
<plugin ... variable=> | Plugin-specific plugins.<Name> block, or variables.gradle / Info.plist |
A representative result:
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.example.myapp',
appName: 'My App',
webDir: 'www',
plugins: {
SplashScreen: {
launchShowDuration: 0,
launchAutoHide: false,
},
},
};
export default config;
4. Platform folder hygiene
Under Cordova, platforms/ and plugins/ were generated and ignored. Under
Capacitor, ios/ and android/ are source — committed, edited in Xcode
and Android Studio, and never regenerated casually.
- Delete
platforms/,plugins/,hooks/,config.xml, andwww/build leftovers from the repo - Remove the old
ionic.config.jsonintegrations.cordovablock - Add
ios/App/Pods,ios/App/build,android/.gradle,android/build,android/app/buildto.gitignore; keep the rest ofios/andandroid/ - Move your Cordova
resources/icons and splashes toassets/and runnpx @capacitor/assets generate - Port any
config.xml<edit-config>and<config-file>entries by hand intoInfo.plistandAndroidManifest.xml— permissions strings (NSCameraUsageDescriptionand friends) are the usual ones
5. Code changes
Replace deviceready with nothing — Capacitor plugins are available as soon
as the web app loads. Replace window.cordova.* calls with plugin imports:
// Before (Cordova, via ionic-native / awesome-cordova-plugins)
// this.camera.getPicture({ destinationType: this.camera.DestinationType.DATA_URL })
// After (Capacitor)
import { Camera, CameraResultType } from '@capacitor/camera';
const photo = await Camera.getPhoto({
resultType: CameraResultType.Uri,
quality: 80,
});
// photo.webPath is safe to bind to <img src>; photo.path is the native path
Search for cordova.file., file://, and cdvfile:// paths — file handling
is the largest source of post-migration bugs (see gotchas below).
6. CI changes
- Replace
ionic cordova build ios/androidwithnpm run build && npx cap syncfollowed byxcodebuild/gradlew— or a fastlane lane that does both - Cache
node_modules,~/.gradle, and (if used) CocoaPods - Add
npx cap syncto the pipeline immediately after the web build; the commonest CI failure after migration is a stale native project - If you used Appflow for builds, this is the moment to move to GitHub Actions, Codemagic, or Bitrise — see our post-Appflow CI/CD tutorial
7. Gotchas we hit on almost every migration
- Splash screen flashes then hides too early or never. Set
launchAutoHide: falseand callSplashScreen.hide()yourself once the first screen has rendered. Cordova'sAutoHideSplashScreenpreference no longer applies. - Status bar overlaps content on Android 15+. Capacitor 8 moved
edge-to-edge handling to the System Bars plugin and CSS
env()variables; the oldadjustMarginsForEdgeToEdgesetting is gone. Budget an afternoon for layout checks on a modern Android device. - File paths.
cordova.file.dataDirectorybecomesDirectory.Datawith@capacitor/filesystem, and a native path must be converted withCapacitor.convertFileSrc()before it can be loaded in the WebView. Stored absolute paths from the Cordova era will be wrong on iOS after the app container moves — store relative paths and a directory enum. window.openand external links. Under Cordova, InAppBrowser intercepted them; under Capacitor, use@capacitor/browserfor external URLs and checkserver.allowNavigationif you rely on navigating the WebView to another origin.- Keyboard behaviour. Cordova's
KeyboardResizepreferences map toplugins.Keyboard.resizeincapacitor.config.ts; test every form on iOS. - Plugins that "work" until release. A kept Cordova plugin may rely on a
config.xmlpreference you no longer have. Test release builds, not just debug.
8. Sign-off
Before the store submission that ships the migrated app:
- Smoke test the full plugin audit table on a physical iOS and Android device
- Verify push registration, deep links, and file uploads end to end
- Confirm the
appId, version and build numbers match the store listing - Tag the last Cordova commit so you can diff against it for the next six months
Need a second pair of hands or a plugin with no Capacitor equivalent rewritten natively? Our Capacitor consultants do this every month.