+1 (415) 843-4662

Upgrading Ionic 4/5 Angular apps to Ionic 8/9

Ionic 4 shipped in early 2019 and Ionic 5 in 2020. Both are long out of support, yet a lot of revenue still runs through apps built on them. This guide is for Angular teams on Ionic 4 or 5 who need to land on Ionic 8 (the current long-lived line) or Ionic 9 (released August 19, 2026). The numbers look scary — four or five major versions — but most of the work is Angular's, not Ionic's, and Ionic now ships a migration tool that handles the mechanical part of the final hop.

Step 0: build the harness before you touch a version number

You cannot judge an upgrade without a baseline. Before upgrading anything:

  • Make sure ng build --configuration production succeeds and record the warnings
  • Write (or revive) end-to-end smoke tests for the five or six user journeys that matter commercially. Playwright running against ionic serve is the cheapest way; a handful of tests is enough
  • Take screenshots of every major screen on iOS and Android. Ionic's visual changes across majors (iOS 13+ styling in v5, the Ionic 7 overlay and toolbar changes, Ionic 8's dark-mode and high-contrast palette) are real, and you will want to know whether a difference is a regression or a new default

Step 1: climb Angular and Ionic together, one major at a time

Ionic majors pin Angular ranges. Going straight from Ionic 4 + Angular 8 to Ionic 9 + Angular 21 skips migrations that ng update and the Ionic changelogs only apply incrementally. Our sequence:

FromToWhat changes
Ionic 4 / Angular 8–9Ionic 5 / Angular 9–10iOS 13 visual refresh; ion-slides and icon changes; @ionic/angular 5 requires Ivy
Ionic 5Ionic 6 / Angular 12–14Overlay components move to ion-* inline usage; ion-datetime rewrite; ion-modal sheet/card; ionicons 6
Ionic 6Ionic 7 / Angular 14–16ion-slides removed (use Swiper directly); component property renames; modal and popover presentation changes
Ionic 7Ionic 8 / Angular 16–19+Dark and high-contrast palettes become opt-in stylesheets; ion-nav and overlay typing tightened; @ionic/angular/standalone matures
Ionic 8Ionic 9 / Angular 18–22Zoneless by default on Angular 21+; standalone becomes the default import path; lazy imports move to @ionic/angular/lazy

Each row has an official page under ionicframework.com/docs/updating. Read it, run ng update @angular/core@N @angular/cli@N for the matching Angular major, run the tests, commit, repeat. Do not batch two hops in one branch; when something breaks you want to know which hop did it.

Commands per hop

# Example: Ionic 6 -> 7 with Angular 14 -> 16
ng update @angular/core@16 @angular/cli@16
npm install @ionic/angular@7 @ionic/angular-toolkit@latest ionicons@latest
npm run build && npx playwright test

Step 2: the breaking changes that actually bite

From a decade of these upgrades, the items that consume the most time:

  • ion-slides removal (Ionic 7). Replace with Swiper's Angular components. Budget half a day per screen that used slides heavily.
  • ion-datetime rewrite (Ionic 6). The old wheel picker became an inline calendar; displayFormat and pickerFormat are gone. Wrap it in a modal or popover if your design needs the old interaction.
  • Overlays (Ionic 6–7). ion-modal with [isOpen], presentingElement, and breakpoints replaces most ModalController boilerplate; type-safe componentProps require cleanup.
  • Theming (Ionic 8). Dark mode is no longer automatic via the prefers-color-scheme variables you pasted from the v4 docs; import @ionic/angular/css/palettes/dark.system.css (or dark.class.css for a toggle). Re-check every hard-coded colour.
  • Icons. ionicons 5+ renamed and removed icons; run a grep for <ion-icon name= and check the list.
  • RxJS and TypeScript. Angular majors drag these along; rxjs-compat removal and strict template checks surface real bugs.

Step 3: standalone components

Ionic 8's @ionic/angular/standalone is the path forward, and in Ionic 9 it is the default: @ionic/angular is the standalone entry point and the old lazy-loaded modules live at @ionic/angular/lazy. Migrate after you are on Ionic 8 and a modern Angular:

ng generate @angular/core:standalone

Angular's schematic converts components, removes NgModules, and bootstraps the app with bootstrapApplication. Then switch Ionic to per-component imports:

import { Component } from '@angular/core';
import {
  IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel,
} from '@ionic/angular/standalone';

@Component({
  selector: 'app-orders',
  standalone: true,
  imports: [IonHeader, IonToolbar, IonTitle, IonContent, IonList, IonItem, IonLabel],
  templateUrl: './orders.page.html',
})
export class OrdersPage {}

and bootstrap with provideIonicAngular():

import { bootstrapApplication } from '@angular/platform-browser';
import { provideRouter } from '@angular/router';
import { provideIonicAngular } from '@ionic/angular/standalone';

bootstrapApplication(AppComponent, {
  providers: [provideIonicAngular({ mode: 'ios' }), provideRouter(routes)],
});

Tree-shaking now works per component, which is typically the single biggest bundle-size win of the whole upgrade.

Step 4: signals and zoneless

Angular signals (stable since Angular 17) and Ionic 9's zoneless default change how state updates reach the template. Under Zone.js, this worked:

async openFilters() {
  const modal = await this.modalCtrl.create({ component: FiltersPage });
  await modal.present();
  const { data } = await modal.onWillDismiss();
  this.filters = data; // Zone.js triggered change detection
}

Without Zone.js, Angular does not know this.filters changed. Make the state a signal and the template updates regardless of how the value arrived:

import { signal } from '@angular/core';

filters = signal<Filters | null>(null);

async openFilters() {
  const modal = await this.modalCtrl.create({ component: FiltersPage });
  await modal.present();
  const { data } = await modal.onWillDismiss<Filters>();
  this.filters.set(data ?? null);
}

Do the same for setTimeout callbacks, RxJS subscriptions that write to fields (or use toSignal()), and Ionic Platform events. Angular's signals guide covers the patterns; Ionic's zoneless guide covers the overlay cases. You can stay on Zone.js in Ionic 9 if you need to — it is still supported — but new code should not depend on it.

Step 5: the final hop with @ionic/migrate

For Ionic 8 → 9, Ionic ships a migration tool that applies the safe breaking changes and lists the rest with file and line numbers:

git status   # must be clean; the tool refuses to run on a dirty tree
npx @ionic/migrate --dry-run
npx @ionic/migrate

It reads your framework and version from package.json, rewrites imports (@ionic/angular/standalone@ionic/angular, lazy imports → @ionic/angular/lazy), bumps the @ionic/* packages, runs Prettier on what it touched, and reinstalls dependencies. Use --check in CI to block merges while migrations are pending. It is single-shot per major, so run it once, review the diff, then work through the printed checklist.

Step 6: after the upgrade

  • Run the screenshot comparison from Step 0 and triage every difference
  • Re-run ng build --configuration production and compare bundle sizes
  • Update the Capacitor major at the same time if you are behind — our Cordova → Capacitor checklist covers the runtime side
  • Record the Ionic and Angular versions and the next planned upgrade window in the repo's README; the reason this app got stuck on Ionic 4 is that nobody wrote that line in 2019

When to call for help

If the app is on Ionic 4 with AngularJS remnants (an Ionic 1 app that was "half migrated"), or the team that wrote it is gone, an incremental rewrite may be cheaper than a five-hop upgrade. That is the call we help teams make on our legacy modernization engagements, and we are happy to look at a package.json and give you an honest read.