Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 127 additions & 29 deletions docs/updating/9-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -340,10 +340,10 @@ For more information on migrating from React Router v5 to v6, refer to the [Reac

### Vue

1. Ionic 9 supports Vue 3.5+ and Vue Router 5. Update Vue and Vue Router:
1. Ionic 9 supports Vue 3.5+. Update to the latest version of Vue:

```shell
npm install vue@latest vue-router@latest
npm install vue@latest
```

2. Update to the latest version of Ionic 9:
Expand All @@ -352,21 +352,18 @@ npm install vue@latest vue-router@latest
npm install @ionic/vue@latest @ionic/vue-router@latest
```

#### Vue Router 5 Migration
### Vue Router

`@ionic/vue-router` 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`.

Vue Router v5 is a transition release that ships no runtime breaking changes for Vue Router v4 consumers, so no application code changes are required for routes, navigation guards, or `IonRouterOutlet`. Bump the dep ranges in your app's `package.json`:
1. Ionic 9 supports Vue Router 5. Update to the latest version of Vue Router:

```diff
"dependencies": {
- "vue": "^3.4.0",
- "vue-router": "^4.0.0"
+ "vue": "^3.5.0",
+ "vue-router": "^5.0.0"
}
```shell
npm install vue-router@latest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
npm install vue-router@latest
npm install vue-router@5

React Router is pinned to @6 further up for the same reason, and this section says v6 removes the next() callback form, so @latest starts installing an unsupported major the day it ships.

```

`@ionic/vue-router` 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`.

Vue Router v5 is a transition release that ships no runtime breaking changes for Vue Router v4 consumers, so no application code changes are required for routes, navigation guards, or `IonRouterOutlet`.

#### Deprecation Warning for `next()` in Navigation Guards

Vue Router v5 prints a deprecation warning when `next()` is called inside `beforeRouteLeave`, `beforeRouteEnter`, `beforeRouteUpdate`, or `router.beforeEach`. The callback form still works, but Vue Router v6 will remove it. Migrate to the return-value pattern:
Expand Down Expand Up @@ -442,18 +439,6 @@ Ionic 9 officially supports Capacitor 7 and later. Native platform detection no

If your app is still on Capacitor 2, it will no longer be detected as running on a native platform, so `isPlatform('capacitor')`, `isPlatform('hybrid')`, and `getPlatforms()` will report `web` instead of native. Upgrade to Capacitor 7 or later by following the [Capacitor updating guides](https://capacitorjs.com/docs/updating/7-0).

### Legacy Picker

1. Remove any usages of the `ion-picker-legacy` and `ion-picker-legacy-column` components. These components have been removed in Ionic 9. The recommended path forward is to use `ion-picker` inside a modal. Review the [Picker in Modal documentation](../api/picker.md#picker-in-modal) for more information.
2. Remove any usages of `pickerController`. If using React, remove any usages of the `useIonPicker` hook. These controller-based APIs have been removed in Ionic 9. Use the [Picker](../api/picker.md) component instead.
3. Remove any usages of the `PickerOptions`, `PickerButton`, `PickerColumn`, and `PickerColumnOption` type exports. These types were associated with the legacy picker and have been removed in Ionic 9.

### Select

The `ionChange` event on `ion-select` now only fires when the value changes. Previously, the `alert` and `action-sheet` interfaces emitted `ionChange` whenever the overlay was confirmed, even if the user picked the option that was already selected. This matches the documented behavior and the existing behavior of the `popover` and `modal` interfaces.

If your code relied on `ionChange` firing on every confirmation (for example, to detect that the user closed the overlay without changing anything), listen for `ionDismiss` instead, or use the `didDismiss` event on the underlying alert or action sheet.

### Img

`ion-img` is deprecated and will be removed in Ionic 10. The component was created to lazy-load images before browsers supported lazy loading natively. Modern browsers now support the [`loading="lazy"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img#loading) attribute on the native `<img>` element, so the component is no longer needed.
Expand Down Expand Up @@ -492,20 +477,49 @@ The native `<img>` element does not emit Ionic's custom events. Use the standard

### Input

#### `autocorrect` Property Type Changed to Boolean

The `autocorrect` property on `ion-input` is now a `boolean` (default `false`) instead of `'on' | 'off'`. Because the attribute coerces to `true` for any value other than the string `"false"`, `autocorrect="off"` now enables autocorrect.

- 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).

### Searchbar
#### Internal DOM Structure Changes

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This heading text repeats under Select and Textarea, so the built page ends up with #internal-dom-structure-changes, -1 and -2, numbered by document order. Same deal for the autocorrect heading across Input and Searchbar. Nothing links to them today so nothing's broken, but since this PR also establishes alphabetical ordering under Required Changes, the next component landing before Input with a DOM section would quietly renumber them all. Qualifying each with its component name would sort it. Just a nit though, no worries if you'd rather leave it.


The `autocorrect` property on `ion-searchbar` is now a `boolean` (default `false`) instead of `'on' | 'off'`. Because the attribute coerces to `true` for any value other than the string `"false"`, `autocorrect="off"` now enables autocorrect.
New wrapper elements have been added to the component's internal DOM structure to support floating labels with slotted start and end content. Additionally, the structure of the component has been reorganized, with some elements now grouped differently than before. This may introduce breaking changes for developers who rely on the component's internal DOM structure or apply custom styling to internal elements.

- 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).
The following internal wrapper elements have been added:

- Added: `<div class="input-start">` wrapper for the start slot
- Added: `<div class="input-control">` wrapper for the label and native control
- Added: `<div class="input-end">` wrapper for the end slot and clear button

While the public API has not changed, selectors or style overrides targeting the previous markup may need to be updated to reference the new wrapper elements and their organization. If you have custom CSS targeting the internal structure of input, update your selectors to account for these structural changes.

### Legacy Picker

The `ion-picker-legacy` and `ion-picker-legacy-column` components have been removed.

- Usages such as `ion-picker-legacy` or `IonPickerLegacy` should be changed to `ion-picker` and `IonPicker`, respectively. Review the [Picker in Modal documentation](../api/picker.md#picker-in-modal) for more information.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
- Usages such as `ion-picker-legacy` or `IonPickerLegacy` should be changed to `ion-picker` and `IonPicker`, respectively. Review the [Picker in Modal documentation](../api/picker.md#picker-in-modal) for more information.
- Replace `ion-picker-legacy` with `ion-picker`, and `ion-picker-legacy-column` with `ion-picker-column`. Since `ion-picker` renders inline instead of as an overlay, present it inside an `ion-modal` to keep the previous experience. Review the [Picker in Modal documentation](../api/picker.md#picker-in-modal) for more information.

I think we lost something important in this rewrite. The old wording said the path forward was ion-picker inside a modal, and now it reads as a straight tag rename, which won't get anyone back to what they had. Following it literally leaves you with a picker in the page where you used to get an overlay. The column mapping went missing too, even though the sentence above still names ion-picker-legacy-column. This is the one thing a reader has to actually do, so I'd want it sorted before approving.

- Remove any usages of `pickerController`. If using React, remove any usages of the `useIonPicker` hook. These controller-based APIs have been removed. Use the [Picker](../api/picker.md) 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.

### Modal

#### `handleBehavior` Default Changed

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>
```

### Nav

#### Router Integration Removed

`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`, and so on) and `ion-nav-link`.

This only affects apps that placed an `ion-nav` inside an `ion-router` (vanilla JavaScript projects) and relied on the router to drive it. If you use `ion-nav` on its own for local, in-page stack navigation, no changes are required.
Expand All @@ -530,6 +544,90 @@ If you relied on `ion-nav` to update the URL, use `ion-router-outlet` for URL-ba

An `ion-nav` can still be nested 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. For a complete, working example, refer to [Using ion-nav within a Routed Page](../api/router.md#using-ion-nav-within-a-routed-page).

### Router Outlet

`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.

### Searchbar

#### `autocorrect` Property Type Changed to Boolean

The `autocorrect` property on `ion-searchbar` is now a `boolean` (default `false`) instead of `'on' | 'off'`. Because the attribute coerces to `true` for any value other than the string `"false"`, `autocorrect="off"` now enables autocorrect.

- 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).

### Select

#### `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.

#### Internal DOM Structure Changes

The component's internal DOM structure has been restructured to support floating labels with slotted start and end content. Additionally, the structure of the component has been reorganized, with some elements now grouped differently than before. The `inner` wrapper element has been removed, and its content has been split across separate wrapper elements for the start slot, control, and end slot. This may introduce breaking changes for developers who rely on the component's internal DOM structure or apply custom styling to internal elements.

Developers who previously styled `ion-select::part(inner)` should migrate to targeting the updated component structure using the following CSS parts instead:

- `ion-select::part(start)` - Target the start slot wrapper
- `ion-select::part(control)` - Target the control wrapper containing the label and native select. When the label is not floating or stacked, this part also contains the dropdown icon.
- `ion-select::part(end)` - Target the end slot wrapper. When the label is floating or stacked, this part also contains the dropdown icon.

### Textarea

#### Internal DOM Structure Changes

The internal DOM structure has been modified to support floating labels with slotted start and end content. Additionally, the structure of the component has been reorganized, with some elements now grouped differently than before. This may introduce breaking changes for developers who rely on the component's internal DOM structure or apply custom styling to internal elements.

The following internal elements have been modified:

- Removed: `<div class="textarea-wrapper-inner">`
- Renamed: `<div class="start-slot-wrapper">` is now `<div class="textarea-start">`
- Added: `<div class="textarea-control">` wrapper for the label and native control
- Renamed: `<div class="end-slot-wrapper">` is now `<div class="textarea-end">`

While the public API has not changed, selectors or style overrides targeting the previous markup will need to be updated to reference the new element names and their organization. If you have custom CSS targeting the internal structure of textarea, update your selectors to account for these structural changes.

#### Minimum Height Change

The minimum height of textarea in Material Design (`md` mode) has been increased from `56px` to `72px`. This change ensures consistent heights across textareas regardless of the `fill` property or `labelPlacement`, providing a more uniform and predictable user experience. If you were relying on textareas being `56px` tall or had custom CSS based on that value, you will need to either update your styles to accommodate the new `72px` height or override it back to `56px` if needed.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
The minimum height of textarea in Material Design (`md` mode) has been increased from `56px` to `72px`. This change ensures consistent heights across textareas regardless of the `fill` property or `labelPlacement`, providing a more uniform and predictable user experience. If you were relying on textareas being `56px` tall or had custom CSS based on that value, you will need to either update your styles to accommodate the new `72px` height or override it back to `56px` if needed.
The minimum height of textarea in Material Design (`md` mode) has been increased from `56px` to `72px`. Textareas are now the same height regardless of the `fill` property or `labelPlacement`. If you were relying on textareas being `56px` tall or had custom CSS based on that value, update your styles for the new `72px` height or override it back to `56px`.

Mostly a wording nit, "ensures" plus "providing a more uniform and predictable user experience" reads like marketing copy next to the rest of the guide. The more useful half: "override it back to 56px" doesn't say what to override, so worth naming the CSS variable or selector if there is one.


## Need Help Upgrading?

Be sure to look at the [Ionic 9 Breaking Changes Guide](https://github.com/ionic-team/ionic-framework/blob/main/BREAKING.md#version-9x) for the complete list of breaking changes. This upgrade guide only covers changes that require action from developers.
Expand Down