Skip to content
252 changes: 223 additions & 29 deletions docs/updating/9-0.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,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 @@ -386,21 +386,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@5
```

`@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 @@ -476,18 +473,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 @@ -526,20 +511,76 @@ The native `<img>` element does not emit Ionic's custom events. Use the standard

### Input

#### `autocorrect` Property Type Changed to Boolean {#input-autocorrect-property-type-changed-to-boolean}
Comment thread
brandyscarney marked this conversation as resolved.

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
#### Floating Label Behavior {#input-floating-label-behavior}
Comment thread
brandyscarney marked this conversation as resolved.

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.
Floating labels no longer automatically float when the input contains slotted content. Labels float only when the input is focused or has a value.

- 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).
#### Internal DOM Structure Changes {#input-internal-dom-structure-changes}
Comment thread
brandyscarney marked this conversation as resolved.

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"] { }
```

### Legacy Picker

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

- 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.
- 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 @@ -564,6 +605,159 @@ 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 {#searchbar-autocorrect-property-type-changed-to-boolean}
Comment thread
brandyscarney marked this conversation as resolved.

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.

#### Floating Label Behavior {#select-floating-label-behavior}
Comment thread
brandyscarney marked this conversation as resolved.

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 {#select-internal-dom-structure-changes}
Comment thread
brandyscarney marked this conversation as resolved.

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-start` — `part="start"`
- `.select-control` — `part="control"`
- `.select-end` — `part="end"`

Removed:

- `.select-wrapper-inner` — `part="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.

### Textarea

#### Floating Label Behavior {#textarea-floating-label-behavior}
Comment thread
brandyscarney marked this conversation as resolved.

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 {#textarea-internal-dom-structure-changes}
Comment thread
brandyscarney marked this conversation as resolved.

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:

| Fill | Label placement | Previous minimum height |
| ------------------ | ----------------------- | ----------------------- |
| default | `start`, `end`, `fixed` | `44px` |
| default | `floating`, `stacked` | `56px` |
| `solid`, `outline` | any | `56px` |

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

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