Back to Ionic Framework

Breaking Changes

BREAKING.md

9.0.032.3 KB
Original Source

Breaking Changes

This is a comprehensive list of the breaking changes introduced in the major version releases of Ionic Framework.

Versions

Version 9.x

<h2 id="version-9x-browser-platform-support">Browser and Platform Support</h2>

This section details the desktop browser, JavaScript framework, and mobile platform versions that are supported by Ionic 9.

Minimum Browser Versions

Desktop BrowserSupported Versions
Chrome89+
Safari16+
Edge89+
Firefox75+

Minimum JavaScript Framework Versions

FrameworkSupported Version
Angular18+
React18 or 19
Vue3.5+

Minimum Mobile Platform Versions

PlatformSupported Version
iOS16+
Android5.1+ with Chromium 89+

Minimum Native Runtime Versions

Native RuntimeSupported Version
Capacitor7+

Ionic's native platform detection no longer checks the Capacitor 2 isNative flag. isCapacitorNative now relies solely on Capacitor.isNativePlatform(), which was added in Capacitor 3. Apps running Capacitor 2 will no longer be detected as a native/hybrid platform, so isPlatform('capacitor'), isPlatform('hybrid'), and getPlatforms() will report web instead of native. Upgrade to a supported Capacitor version (7 or later).

<h2 id="version-9x-package-exports">Package Exports</h2>

@ionic/core's package.json now declares an exports field. Subpaths like @ionic/core/components and @ionic/core/loader previously failed under Node ESM (Angular 21's default Vitest builder, raw Node, etc.) with ERR_UNSUPPORTED_DIR_IMPORT, because the strict ESM resolver doesn't read the nested package.json files this package relied on. The new exports map declares the documented subpaths explicitly.

exports is an allowlist. Apps using Node ESM, webpack 5, or TypeScript moduleResolution: "bundler"/"node16"/"nodenext" that import from undocumented internal paths need to switch to one of the supported subpaths:

SubpathUse
@ionic/coreRoot entry, controllers, animation builders
@ionic/core/componentsCustom-element constructors and shared utilities
@ionic/core/components/ion-*.jsSingle-component custom-element constructor
@ionic/core/loaderdefineCustomElements lazy loader
@ionic/core/hydrateSSR hydration entry
@ionic/core/css/*.cssGlobal stylesheets and palettes

Apps on moduleResolution: "node" (classic) and webpack 4 keep resolving through the legacy fields and are unaffected.

<h2 id="version-9x-components">Components</h2> <h4 id="version-9x-input">Input</h4>

autocorrect Property Type Changed to Boolean

The autocorrect property on ion-input is now a boolean and defaults to false. It was previously typed as 'on' | 'off' with a default of 'off'. This resolves a type conflict introduced when TypeScript 5.9 added autocorrect: boolean to the DOM HTMLElement interface.

The string form no longer behaves the same way. Because an HTML attribute coerces to true for any non-empty string, autocorrect="off" now evaluates to true (autocorrect enabled). Migrate to the boolean property:

  • Remove the attribute to keep autocorrect disabled (the default).
  • Use a property binding to enable it: [autocorrect]="true" (Angular), autocorrect={true} (React), or :autocorrect="true" (Vue).

Floating Label Behavior

Floating labels no longer automatically float when the input contains slotted content. Labels float only when the input is focused or has a value.

Internal DOM Structure Changes

The internal DOM structure has been reorganized to support floating labels with slotted content.

Added:

  • .input-start
  • .input-control
  • .input-end

Restructured:

  • .label-text-wrapper moved from .input-wrapper into .input-control
  • .native-wrapper moved from .input-wrapper into .input-control
  • Start slot moved from .native-wrapper into .input-start
  • Clear button icon moved from .native-wrapper into .input-end
  • End slot moved from .native-wrapper into .input-end
  • .input-control now contains the label text and native input, while start/end content is separated into dedicated wrappers

Update your selectors to account for these structural changes:

diff
-ion-input .input-wrapper .native-wrapper { }
+ion-input .input-control .native-wrapper { }

-ion-input .input-wrapper .native-wrapper [slot="start"] { }
+ion-input .input-start [slot="start"] { }

-ion-input .input-wrapper .native-wrapper .input-clear-icon { }
+ion-input .input-end .input-clear-icon { }

-ion-input .input-wrapper .native-wrapper [slot="end"] { }
+ion-input .input-end [slot="end"] { }
<h4 id="version-9x-legacy-picker">Legacy Picker</h4>
  • ion-picker-legacy and ion-picker-legacy-column have been removed. The legacy picker component has been replaced with an inline picker component.
    • Usages such as ion-picker-legacy or IonPickerLegacy should be changed to ion-picker and IonPicker, respectively.
  • Remove any usages of pickerController. If using React, remove any usages of the useIonPicker hook. These controller-based APIs have been removed. Use the inline picker component instead.
  • Remove any usages of the PickerOptions, PickerButton, PickerColumn, and PickerColumnOption type exports. These types were associated with the legacy picker and have been removed.
<h4 id="version-9x-modal">Modal</h4>

The handleBehavior property on ion-modal now defaults to "cycle" instead of "none". For sheet modals that display a handle, this means the handle is now focusable and activating it (by click, keyboard, or screen reader) cycles the sheet through its available breakpoints. This matches the native iOS sheet behavior and keeps sheet modals operable for assistive technology users by default.

Sheet modals that relied on the handle being inert should set handleBehavior="none" to restore the previous behavior:

html
<ion-modal handle-behavior="none"></ion-modal>
<h4 id="version-9x-nav">Nav</h4>

ion-nav no longer integrates with ion-router. It is now a standalone imperative stack navigation component, driven only through its own API (root, push, pop, setRoot, etc.) and ion-nav-link.

The following behaviors have been removed:

  • The router no longer discovers or drives an ion-nav. Placing an ion-nav inside an ion-router no longer turns it into a routed outlet.
  • Navigating an ion-nav (via push, pop, ion-nav-link, or the swipe-to-go-back gesture) no longer updates the URL, and the router's navigation guards no longer run for ion-nav transitions.
  • The internal setRouteId() and getRouteId() methods and the updateURL nav option have been removed.

Apps that relied on ion-nav to update the URL (for example, pushing components and expecting the browser URL to change) should use ion-router-outlet for URL-based routing. Keep the ion-route definitions and swap the outlet element:

diff
  <ion-router>
    <ion-route url="/" component="page-one"></ion-route>
    <ion-route url="/page-two" component="page-two"></ion-route>
  </ion-router>

- <ion-nav></ion-nav>
+ <ion-router-outlet></ion-router-outlet>

An ion-nav can still be used inside a routed page for local, URL-less stack navigation. It manages its own stack via root and ion-nav-link, and the URL never changes as you push and pop:

html
<!-- Inside a routed page, an ion-nav manages a local, URL-less stack -->
<ion-nav root="page-one"></ion-nav>

<script>
  // Each view is a standard custom element. Setting `root` renders the first
  // view, and `ion-nav-link` pushes the next one. The URL never changes.
  customElements.define(
    'page-one',
    class extends HTMLElement {
      connectedCallback() {
        this.innerHTML = `
          <ion-header>
            <ion-toolbar><ion-title>Page One</ion-title></ion-toolbar>
          </ion-header>
          <ion-content class="ion-padding">
            <ion-nav-link router-direction="forward" component="page-two">
              <ion-button>Go to Page Two</ion-button>
            </ion-nav-link>
          </ion-content>
        `;
      }
    }
  );

  customElements.define(
    'page-two',
    class extends HTMLElement {
      connectedCallback() {
        this.innerHTML = `
          <ion-header>
            <ion-toolbar>
              <ion-buttons slot="start"><ion-back-button></ion-back-button></ion-buttons>
              <ion-title>Page Two</ion-title>
            </ion-toolbar>
          </ion-header>
          <ion-content class="ion-padding">Page Two content</ion-content>
        `;
      }
    }
  );
</script>
<h4 id="version-9x-router-outlet">Router Outlet</h4>

ion-router-outlet now exposes a swipeGesture property that controls the swipe-to-go-back gesture per outlet. This property defaults to true in "ios" mode and false in "md" mode.

swipeBackEnabled Config Behavior Change

In React and Vue, the swipeBackEnabled config option is now read once when the outlet mounts. Apps that dynamically toggle this config value at runtime should migrate to the swipeGesture property instead:

React:

diff
- setupIonicReact({ swipeBackEnabled: someCondition });
+ <IonRouterOutlet swipeGesture={someCondition} />

Vue:

diff
- createApp(App).use(IonicVue, { swipeBackEnabled: someCondition })
+ <ion-router-outlet :swipe-gesture="someCondition" />

Disabling Swipe-to-Go-Back

To disable the gesture on a specific outlet, set swipeGesture to false:

tsx
<IonRouterOutlet swipeGesture={false} />

The swipeBackEnabled config option is still respected as the initial default and does not need to change for apps that set it once at startup.

<h4 id="version-9x-searchbar">Searchbar</h4>

The autocorrect property on ion-searchbar is now a boolean and defaults to false. It was previously typed as 'on' | 'off' with a default of 'off'. This resolves a type conflict introduced when TypeScript 5.9 added autocorrect: boolean to the DOM HTMLElement interface.

The string form no longer behaves the same way. Because an HTML attribute coerces to true for any non-empty string, autocorrect="off" now evaluates to true (autocorrect enabled). Migrate to the boolean property:

  • Remove the attribute to keep autocorrect disabled (the default).
  • Use a property binding to enable it: [autocorrect]="true" (Angular), autocorrect={true} (React), or :autocorrect="true" (Vue).
<h4 id="version-9x-select">Select</h4>

ionChange Only Fires When the Value Changes

The ionChange event on ion-select now only fires when the selected value actually changes. Previously, the alert and action-sheet interfaces emitted ionChange every time the overlay was confirmed, even when the user chose the option that was already selected. This aligns the alert and action-sheet interfaces with the existing behavior of the popover and modal interfaces, and with the documented contract of ionChange.

Apps that relied on ionChange firing on every confirmation (for example, to detect overlay dismissal without a value change) should listen for ionDismiss instead, or use the didDismiss event on the underlying alert or action sheet.

Action Sheet Interface selected Role Removed

When using interface="action-sheet", ion-select no longer assigns the selected role to the action sheet button for the currently selected option. This aligns the action-sheet interface with the alert, popover, and modal interfaces, none of which assign this role. This does not change the selected option's styling.

Previously, the selected role was assigned only to the option matching the select's current value. Because the dismiss role mirrors the tapped button, this surfaced in just one case: re-selecting the already-selected option dismissed the action sheet with role: "selected" in ionActionSheetDidDismiss. Tapping any other option changed the value and dismissed with role: "". Now that the role is no longer assigned, both cases dismiss with role: undefined. Apps that inspected this role to detect that a value was chosen, such as reading role from the underlying action sheet's onDidDismiss result, should listen for ion-select's ionChange event instead, which emits the selected value when the selection changes.

Floating Label Behavior

Floating labels no longer automatically float when the select contains slotted content. Labels float only when the select is focused or has a value. Additionally, when using a floating label, the placeholder is only visible when the select is focused.

Internal DOM Structure Changes

The internal DOM structure has been reorganized to support floating labels with slotted content. This changes the structure and location of several exposed shadow parts.

Added:

  • .select-startpart="start"
  • .select-controlpart="control"
  • .select-endpart="end"

Removed:

  • .select-wrapper-innerpart="inner"

Restructured:

  • .label-text-wrapper remains part="label" but moved from .select-wrapper into .select-control
  • .native-wrapper remains part="container" but moved from .select-wrapper-inner into .select-control
  • Start slot moved from .select-wrapper-inner into .select-start (part="start")
  • End slot moved from .select-wrapper-inner into .select-end (part="end")
  • .select-icon remains part="icon" but its location depends on the label state:
    • With a start/end label, the icon is inside .native-wrapper
    • With a floating/stacked label, the icon is inside .select-end

Update selectors that target the exposed shadow parts to account for the new structure:

If you currently target part="inner", that part has been removed. Update those styles to target the new parts as appropriate.

If you target part="label", part="container", or part="icon", the part names remain unchanged, but their position in the shadow DOM has changed. This may affect styles that depend on the relationship or layout of these parts.

Use the new part="start", part="control", and part="end" parts to target the new structural wrappers.

<h4 id="version-9x-textarea">Textarea</h4>

Floating Label Behavior

Floating labels no longer automatically float when the textarea contains slotted content. Labels float only when the textarea is focused or has a value.

Internal DOM Structure Changes

The internal DOM structure has been reorganized to support floating labels with slotted content.

Removed: .textarea-wrapper-inner

Added: .textarea-control

Renamed:

  • .start-slot-wrapper.textarea-start
  • .end-slot-wrapper.textarea-end

Restructured:

  • .label-text-wrapper moved from .textarea-wrapper-inner into .textarea-control
  • .native-wrapper moved from .textarea-wrapper-inner into .textarea-control
  • .start-slot-wrapper moved from .textarea-wrapper-inner to .textarea-wrapper and was renamed .textarea-start
  • .end-slot-wrapper moved from .textarea-wrapper-inner to .textarea-wrapper and was renamed .textarea-end

Update your selectors to account for these structural changes:

diff
-ion-textarea .textarea-wrapper-inner .native-wrapper { }
+ion-textarea .textarea-control .native-wrapper { }

-ion-textarea .start-slot-wrapper [slot="start"] { }
+ion-textarea .textarea-start [slot="start"] { }

-ion-textarea .end-slot-wrapper [slot="end"] { }
+ion-textarea .textarea-end [slot="end"] { }

Minimum Height Change

The minimum height of textarea in Material Design (md mode) is now 72px. At the default number of rows this makes textareas the same height regardless of the fill property or labelPlacement. Previously the minimum height was:

FillLabel placementPrevious minimum height
defaultstart, end, fixed44px
defaultfloating, stacked56px
solid, outlineany56px

These were minimums, not the heights textareas actually rendered at. A textarea with content in the start or end slots was already taller than its minimum, so the change affects it differently. For example, a fill="solid" textarea with slotted icons and buttons previously rendered at 72px with a start label and 81px with a floating label. Both are now 72px, so that floating label case is 9px shorter than before rather than taller.

Because 72px is taller than two rows of text, rows values below 3 no longer change the height of the textarea in md mode: rows="1" and rows="2" both render at 72px.

If you were relying on the previous heights, or you need rows to control the height, override the minimum height back. The override has to be more specific than the component's own style, so a bare ion-textarea selector will not apply. Add a custom class to the textarea to increase specificity:

css
/* Add a custom class to the textarea */
ion-textarea.custom {
  min-height: 44px;
}
<h2 id="version-9x-framework-specific">Framework Specific</h2> <h4 id="version-9x-angular">Angular</h4>

Minimum Angular Version

Ionic 9 requires Angular 18 or later. Angular 16 and 17 are no longer supported.

Standalone Components Imported by Default

Following industry standards, Ionic 9 makes standalone components the default import path. Standalone component imports have changed from @ionic/angular/standalone to @ionic/angular. Lazy-loaded component imports have changed from @ionic/angular to @ionic/angular/lazy.

IonicModule Deprecation

IonicModule is deprecated in Ionic 9 and will be removed in a future major version. It remains fully functional in Ionic 9, so existing applications continue to work without changes.

Applications should migrate to provideIonicAngular(), which works in both standalone and NgModule-based applications. For an NgModule-based app, replace IonicModule.forRoot(config) in the imports array with provideIonicAngular(config) in the providers array. Any config passed to IonicModule.forRoot() can be passed as an object to provideIonicAngular(). Refer to the build options guide for migration steps.

Zoneless Change Detection by Default

Ionic 9 defaults to zoneless change detection. Angular 21 bootstraps zoneless out of the box, so a new Ionic 9 app on Angular 21 runs without Zone.js and requires no change-detection provider. The ng add @ionic/angular schematic no longer registers provideZoneChangeDetection().

Because Zone.js no longer triggers change detection automatically, component state that you update from an asynchronous callback that Angular doesn't wrap (awaiting an overlay result such as modal.onWillDismiss(), setTimeout, RxJS subscriptions, Platform events) no longer re-renders on its own. Update a signal or call ChangeDetectorRef.markForCheck() in those callbacks. Template event bindings, @HostListener, reactive forms, and Ionic lifecycle hooks (ionViewWillEnter, etc.) that set state synchronously are unaffected. Refer to the Zoneless Change Detection guide for the patterns.

On Angular 18 through 20, Zone.js remains Angular's default, so those versions are unaffected and require no change. To adopt zoneless there, add provideZonelessChangeDetection() (named provideExperimentalZonelessChangeDetection() on Angular 18 and 19).

Keeping Zone.js on Angular 21 (optional)

To keep using Zone.js on Angular 21, opt back in with provideZoneChangeDetection() and keep zone.js in your polyfills.

Standalone bootstrap:

diff
  import { bootstrapApplication } from '@angular/platform-browser';
+ import { provideZoneChangeDetection } from '@angular/core';

  bootstrapApplication(AppComponent, {
    providers: [
+     provideZoneChangeDetection(),
      // ...other providers
    ],
  });

NgModule bootstrap:

diff
  import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
+ import { provideZoneChangeDetection } from '@angular/core';

  platformBrowserDynamic()
-   .bootstrapModule(AppModule)
+   .bootstrapModule(AppModule, {
+     applicationProviders: [provideZoneChangeDetection()],
+   })
    .catch((err) => console.error(err));

Angular forbids provideZoneChangeDetection() inside an NgModule's providers array, so for NgModule apps it must be passed as applicationProviders on the bootstrapModule() call. Both paths also require zone.js in your polyfills, which Angular 21's default scaffold omits:

ts
// src/polyfills.ts
import 'zone.js';

OnPush by Default on Angular 22

Angular 22 changes the default change detection strategy to OnPush for components that don't declare one. Combined with the zoneless default above, any component state that you mutate as a plain field from an Ionic lifecycle hook (ionViewWillEnter, etc.) no longer re-renders on its own. Run ng update, which migrates existing components to eager change detection and preserves the previous behavior, or use a signal (or ChangeDetectorRef.markForCheck()) for state set in those hooks. Ionic's own Angular components already declare OnPush explicitly and are unaffected. Angular 18 through 21 keep the eager default, so they require no change.

TypeScript

Ionic 9 supports TypeScript 5.4 or later, matching the minimum for Angular 18. Angular 21 requires TypeScript 5.9 or later, and Angular 22 requires TypeScript 6.0 or later, per Angular's own requirements.

Node.js

Angular 22 raises the minimum Node.js version to ^22.22.3 || ^24.15.0 || ^26.0.0. Angular 18 through 21 are unaffected.

Module Resolution

@ionic/angular is now published with exports-based subpath resolution. Apps using TypeScript moduleResolution: "node" (classic) can fail to resolve subpaths such as @ionic/angular/lazy. Set moduleResolution to "bundler" (the default for ng new on Angular 17 and later). Refer to Package Exports.

CSS Imports No Longer Use the ~ Prefix

Angular's current build pipeline no longer supports the webpack-loader ~ prefix in CSS @import statements:

diff
- @import '~@ionic/angular/css/core.css';
+ @import '@ionic/angular/css/core.css';
<h4 id="version-9x-react">React</h4>

The @ionic/react and @ionic/react-router packages now require React 18 or 19. React 17 is no longer supported.

The @ionic/react-router package now requires React Router v6. React Router v5 is no longer supported.

Minimum Version Requirements

PackageSupported Version
react18 or 19
react-dom18 or 19
react-router6.4.0+
react-router-dom6.4.0+

TypeScript

The @ionic/react package now requires TypeScript 5.4 or later. Its type definitions use NoInfer, which TypeScript added in 5.4. This matches the minimum that @ionic/angular already requires.

Typed Overlay Hook Props

The useIonModal and useIonPopover hooks type componentProps against the component they are given, instead of accepting any. Props that do not match the component are a compile error, and componentProps is required when the component declares required props. Applications passing incorrect props will see new type errors at build time rather than failing at runtime.

diff
  const Modal: React.FC<{ title: string }> = ({ title }) => <IonContent>{title}</IonContent>;

- const [present, dismiss] = useIonModal(Modal, { subtitle: 'Wrong' });
+ const [present, dismiss] = useIonModal(Modal, { title: 'Hello' });

Props are read from the component rather than from componentProps, so a component declared inline needs its props annotated:

diff
- const [present, dismiss] = useIonModal(({ name }) => <div>Hello {name}.</div>, { name: 'Dave' });
+ const [present, dismiss] = useIonModal(({ name }: { name: string }) => <div>Hello {name}.</div>, { name: 'Dave' });

Passing a JSX element rather than a component is unchanged, and componentProps is not type checked in that case.

npx @ionic/migrate reports the calls this affects and names what is wrong with each, but does not rewrite them, since the right fix depends on what the call was meant to do. For the inline case above, --experimental can annotate the parameter from the componentProps object literal being passed.

React Router v6 introduces several API changes that will require updates to your application's routing configuration:

Route Definition Changes

The component prop has been replaced with the element prop, which accepts JSX:

diff
- <Route path="/home" component={Home} exact />
+ <Route path="/home" element={<Home />} />

Redirect Changes

The <Redirect> component has been replaced with <Navigate>:

diff
- import { Redirect } from 'react-router-dom';
+ import { Navigate } from 'react-router-dom';

- <Redirect to="/home" />
+ <Navigate to="/home" replace />

Nested Route Paths

Routes that contain nested routes or child IonRouterOutlet components need a /* suffix to match sub-paths:

diff
- <Route path="/tabs" element={<Tabs />} />
+ <Route path="/tabs/*" element={<Tabs />} />

Accessing Route Parameters

Route parameters are now accessed via the useParams hook instead of props:

diff
- import { RouteComponentProps } from 'react-router-dom';
+ import { useParams } from 'react-router-dom';

- const MyComponent: React.FC<RouteComponentProps<{ id: string }>> = ({ match }) => {
-   const id = match.params.id;
+ const MyComponent: React.FC = () => {
+   const { id } = useParams<{ id: string }>();

RouteComponentProps Removed

The RouteComponentProps type and its history, location, and match props are no longer available in React Router v6. Use the equivalent hooks instead:

  • history -> useNavigate (see below) or useIonRouter
  • match.params -> useParams (covered above)
  • location -> useLocation
diff
- import { RouteComponentProps } from 'react-router-dom';
+ import { useNavigate, useLocation } from 'react-router-dom';
+ import { useIonRouter } from '@ionic/react';

- const MyComponent: React.FC<RouteComponentProps> = ({ history, location }) => {
-   history.push('/path');
-   history.replace('/path');
-   history.goBack();
-   console.log(location.pathname);
+ const MyComponent: React.FC = () => {
+   const navigate = useNavigate();
+   const router = useIonRouter();
+   const location = useLocation();
+   // In an event handler or useEffect:
+   navigate('/path');
+   navigate('/path', { replace: true });
+   router.goBack();
+   console.log(location.pathname);

Exact Prop Removed

The exact prop is no longer needed. React Router v6 routes match exactly by default. To match sub-paths, use a /* suffix on the path:

diff
- <Route path="/home" exact />
+ <Route path="/home" />

Render Prop Removed

The render prop has been replaced with the element prop:

diff
- <Route path="/foo" render={(props) => <Foo {...props} />} />
+ <Route path="/foo" element={<Foo />} />

Programmatic Navigation

The useHistory hook has been replaced with useNavigate:

diff
- import { useHistory } from 'react-router-dom';
+ import { useNavigate } from 'react-router-dom';
+ import { useIonRouter } from '@ionic/react';

- const history = useHistory();
+ const navigate = useNavigate();
+ const router = useIonRouter();

- history.push('/path');
+ navigate('/path');

- history.replace('/path');
+ navigate('/path', { replace: true });

- history.goBack();
+ router.goBack();

Custom History Prop Removed

The history prop has been removed from IonReactRouter, IonReactHashRouter, and IonReactMemoryRouter. React Router v6's BrowserRouter, HashRouter, and MemoryRouter no longer accept custom history objects.

diff
- import { createBrowserHistory } from 'history';
- const history = createBrowserHistory();
- <IonReactRouter history={history}>
+ <IonReactRouter>

For IonReactMemoryRouter (commonly used in tests), use initialEntries instead:

diff
- import { createMemoryHistory } from 'history';
- const history = createMemoryHistory({ initialEntries: ['/start'] });
- <IonReactMemoryRouter history={history}>
+ <IonReactMemoryRouter initialEntries={['/start']}>

IonRedirect Removed

The IonRedirect component has been removed. Use React Router's <Navigate> component instead:

diff
- import { IonRedirect } from '@ionic/react';
- <IonRedirect path="/old" to="/new" exact />
+ import { Navigate } from 'react-router-dom';
+ <Route path="/old" element={<Navigate to="/new" replace />} />

Path Regex Constraints Removed

React Router v6 no longer supports regex constraints in path parameters (e.g., /:tab(sessions)). Use literal paths instead:

diff
- <Route path="/:tab(sessions)" component={SessionsPage} />
- <Route path="/:tab(sessions)/:id" component={SessionDetail} />
+ <Route path="/sessions" element={<SessionsPage />} />
+ <Route path="/sessions/:id" element={<SessionDetail />} />

IonRoute API Changes

The IonRoute component follows the same API changes as React Router's <Route>. The render prop has been replaced with element, and the exact prop has been removed:

diff
- <IonRoute path="/foo" exact render={(props) => <Foo {...props} />} />
+ <IonRoute path="/foo" element={<Foo />} />

For more information on migrating from React Router v5 to v6, refer to the React Router v6 Upgrade Guide.

<h4 id="version-9x-vue">Vue</h4>

The @ionic/vue-router package now requires Vue Router v5. Vue Router v4 is no longer supported. Vue Router v5 also raises its peer requirement on Vue itself, so the minimum supported Vue version moves to 3.5.0.

Minimum Version Requirements

PackageSupported Version
vue-router5.0.0+
vue3.5.0+

Migration

Vue Router 5 is a transition release that ships no runtime breaking changes for Vue Router 4 consumers, so no application code changes are required for routes, navigation guards, or the IonRouterOutlet. Bump the dep ranges in your app's package.json:

diff
  "dependencies": {
-   "vue": "^3.4.0",
-   "vue-router": "^4.0.0"
+   "vue": "^3.5.0",
+   "vue-router": "^5.0.0"
  }

Deprecation Warning for next() in Navigation Guards

Vue Router 5 prints a deprecation warning when next() is called inside beforeRouteLeave, beforeRouteEnter, beforeRouteUpdate, or router.beforeEach. The callback form still works, but Vue Router 6 will remove it. Migrate to the return-value pattern:

diff
  // Composition API
  onBeforeRouteLeave((to, from) => {
-   if (!confirm('Leave?')) return next(false);
-   next();
+   if (!confirm('Leave?')) return false;
+   return true;
  });
diff
  // Options API
  beforeRouteLeave(to, from, next) {
-   if (!confirm('Leave?')) return next(false);
-   next();
+ beforeRouteLeave(to, from) {
+   if (!confirm('Leave?')) return false;
+   return true;
  }

For more information on Vue Router 5, refer to the Vue Router v4-to-v5 migration guide.