Skip to content
Open
Show file tree
Hide file tree
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
81 changes: 81 additions & 0 deletions docs/api/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Methods from '@ionic-internal/component-api/v8/modal/methods.md';
import Parts from '@ionic-internal/component-api/v8/modal/parts.md';
import CustomProps from '@ionic-internal/component-api/v8/modal/custom-props.mdx';
import Slots from '@ionic-internal/component-api/v8/modal/slots.md';
import SheetDragEvents from '@site/static/usage/v8/modal/sheet/drag-events/index.md';

<head>
<title>ion-modal: Ionic Mobile App Custom Modal API Component</title>
Expand Down Expand Up @@ -210,6 +211,33 @@ A few things to keep in mind when creating custom dialogs:
* `ion-content` is intended to be used in full-page modals, cards, and sheets. If your custom dialog has a dynamic or unknown size, `ion-content` should not be used.
* Creating custom dialogs provides a way of ejecting from the default modal experience. As a result, custom dialogs should not be used with card or sheet modals.

## Event Handling

### Using Drag Events

When using card or sheet modals, Ionic emits several events related to the dragging gesture. These events allow developers to perform specific actions or UI updates based on the movement of the modal.

#### Using `ionDragStart`

The `ionDragStart` event is emitted as soon as the user begins a dragging gesture on the modal. This event fires at the moment the user initiates contact with the handle or modal surface, before any actual displacement occurs. It is particularly useful for preparing the interface for a transition, such as blurring background content or disabling certain interactive elements to ensure a smooth dragging experience.

#### Using `ionDragMove`

The `ionDragMove` event is emitted continuously while the user is actively dragging the modal. This event provides a `ModalDragEventDetail` [object](#modaldrageventdetail) containing real-time data:

- `currentY` and `deltaY`: Track the absolute position and the change in distance since the last frame, useful for calculating drag direction.
- `velocityY`: Measures the speed of the drag, which can be used to trigger specific animations if a user "flicks" the modal.
- `progress`: A normalized value between 0 and 1 representing how far the modal is open. This is ideal for dynamically adjusting the opacity of an overlay or scaling background content as the modal moves.
- `predictedBreakpoint`: For sheet modals, this identifies which breakpoint the modal will snap to if released at that moment.

This event is essential for creating highly responsive UI updates that react instantly to the user's touch. For example, the `progress` value can be used to dynamically darken the backdrop's opacity as the modal is dragged upward.

#### Using `ionDragEnd`

The `ionDragEnd` event is emitted when the user completes the dragging gesture by releasing the modal. Like the move event, it includes the final `ModalDragEventDetail` [object](#modaldrageventdetail). This event is commonly used to finalize state changes once the modal has come to a rest. For example, you might use the `predictedBreakpoint` property to determine which content to load or to update the application's routing state once the user has finished swiping the sheet to a specific height.

<SheetDragEvents />

## Interfaces

### ModalOptions
Expand Down Expand Up @@ -251,6 +279,59 @@ interface ModalCustomEvent extends CustomEvent {
}
```

### ModalDragEventDetail

When using the `ionDragMove` and `ionDragEnd` events, the event detail contains the following properties:

```typescript
interface ModalDragEventDetail {
/**
* The current Y position of the modal.
*
* This can be used to determine how far the modal has been dragged.
*/
currentY: number;
/**
* The change in Y position since the gesture started.
*
* This can be used to determine the direction of the drag.
*/
deltaY: number;
/**
* The velocity of the drag in the Y direction.
*
* This can be used to determine how fast the modal is being dragged.
*/
velocityY: number;
/**
* A number between 0 and 1.
*
* In a sheet modal, progress represents the relative position between
* the lowest and highest defined breakpoints.
*
* In a card modal, it measures the relative position between the
* bottom of the screen and the top of the modal when it is fully
* open.
*
* This can be used to style content based on how far the modal has
* been dragged.
*/
progress: number;
/**
* If the modal is a sheet modal, this will be the breakpoint that
* the modal will snap to if the user lets go of the modal at the
* current moment.
*
* If it's a card modal, this property will not be included in the
* event payload.
*
* This can be used to style content based on where the modal will
* snap to upon release.
*/
predictedBreakpoint?: number;
}
```

## Accessibility

### Keyboard Interactions
Expand Down
4 changes: 2 additions & 2 deletions static/code/stackblitz/v8/angular/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
"@angular/platform-browser": "^20.0.0",
"@angular/platform-browser-dynamic": "^20.0.0",
"@angular/router": "^20.0.0",
"@ionic/angular": "8.7.14",
"@ionic/core": "8.7.14",
"@ionic/angular": "8.7.17-dev.11772118942.181221d4",
"@ionic/core": "8.7.17-dev.11772118942.181221d4",
"ionicons": "8.0.13",
"rxjs": "^7.8.1",
"tslib": "^2.5.0",
Expand Down
2 changes: 1 addition & 1 deletion static/code/stackblitz/v8/html/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"start": "vite preview"
},
"dependencies": {
"@ionic/core": "8.7.14",
"@ionic/core": "8.7.17-dev.11772118942.181221d4",
"ionicons": "8.0.13"
},
"devDependencies": {
Expand Down
4 changes: 2 additions & 2 deletions static/code/stackblitz/v8/react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@ionic/react": "8.7.14",
"@ionic/react-router": "8.7.14",
"@ionic/react": "8.7.17-dev.11772118942.181221d4",
"@ionic/react-router": "8.7.17-dev.11772118942.181221d4",
"@types/node": "^24.0.0",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
Expand Down
4 changes: 2 additions & 2 deletions static/code/stackblitz/v8/vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"preview": "vite preview"
},
"dependencies": {
"@ionic/vue": "8.7.14",
"@ionic/vue-router": "8.7.14",
"@ionic/vue": "8.7.17-dev.11772118942.181221d4",
"@ionic/vue-router": "8.7.17-dev.11772118942.181221d4",
"vue": "^3.2.25",
"vue-router": "5.0.1"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
```html
<ion-header>
<ion-toolbar>
<ion-title>App</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button id="open-modal" expand="block">Open Sheet Modal</ion-button>

<ion-modal
#modal
trigger="open-modal"
[initialBreakpoint]="0.25"
[breakpoints]="[0, 0.25, 0.5, 0.75, 1]"
(ionDragStart)="onDragStart()"
(ionDragMove)="onDragMove($event)"
(ionDragEnd)="onDragEnd($event)"
>
<ng-template>
<ion-content class="ion-padding">
<div class="ion-margin-top">
<ion-label>Drag the handle to adjust the modal's opacity based on a custom max opacity.</ion-label>
</div>
</ion-content>
</ng-template>
</ion-modal>
</ion-content>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
```ts
import { Component, ElementRef, ViewChild } from '@angular/core';
import { IonButton, IonContent, IonHeader, IonLabel, IonModal, IonTitle, IonToolbar } from '@ionic/angular/standalone';
import type { ModalDragEventDetail } from '@ionic/angular/standalone';

@Component({
selector: 'app-example',
templateUrl: 'example.component.html',
standalone: true,
imports: [IonButton, IonContent, IonHeader, IonLabel, IonModal, IonTitle, IonToolbar],
})
export class ExampleComponent {
@ViewChild('modal', { read: ElementRef })
modal!: ElementRef<HTMLIonModalElement>;

private baseOpacity!: number;
private readonly MAX_OPACITY = 0.8;

onDragStart() {
const modalEl = this.modal.nativeElement;
const style = getComputedStyle(modalEl);

// Fetch the current variable value
this.baseOpacity = parseFloat(style.getPropertyValue('--backdrop-opacity'));

// Ensure transitions are off during the active drag
modalEl.style.transition = 'none';
}

onDragMove(event: CustomEvent<ModalDragEventDetail>) {
// `progress` is a value from 1 (top) to 0 (bottom)
const { progress } = event.detail;
const modalEl = this.modal.nativeElement;
const initialBreakpoint = modalEl.initialBreakpoint!;
let dynamicOpacity: number;

/**
* Dragging Down: Progress is between 0 and the initial breakpoint
*/
if (progress <= initialBreakpoint) {
/**
* Calculate how far down the user has dragged from the initial
* breakpoint as a ratio
*/
const downwardProgressRatio = progress / initialBreakpoint;
/**
* Multiplying this by `baseOpacity` ensures that as progress hits 0,
* the opacity also hits 0 without a sudden jump
*/
dynamicOpacity = downwardProgressRatio * this.baseOpacity;
} else {
/**
* Dragging Up: Progress is between the initial breakpoint and 1.0
*/

/**
* Calculate how far up the user has dragged from the initial
* breakpoint as a ratio
*/
const distanceFromStart = progress - initialBreakpoint;
/**
* Calculate the total available space to drag up from the initial
* breakpoint to the top (1.0)
*/
const range = 1 - initialBreakpoint;
/**
* Normalizes that distance into a 0.0 to 1.0 value relative to
* the available upward space
*/
const currentGain = distanceFromStart / range;

// Scale from `baseOpacity` up to `MAX_OPACITY`
dynamicOpacity = this.baseOpacity + currentGain * (this.MAX_OPACITY - this.baseOpacity);
}

modalEl.style.setProperty('--backdrop-opacity', dynamicOpacity.toString());
}

onDragEnd(event: CustomEvent<ModalDragEventDetail>) {
// `currentBreakpoint` tells us which snap point the modal will animate to after the drag ends
const { currentBreakpoint } = event.detail;
const modalEl = this.modal.nativeElement;

/**
* If the modal is snapping to the closed state (0), reset the
* styles.
*/
if (currentBreakpoint === 0) {
modalEl.style.removeProperty('--backdrop-opacity');
return;
}

// Determine the target opacity for the snap-back animation
let targetOpacity = this.baseOpacity;
/**
* If snapping to the top (1), set opacity to MAX_OPACITY for a nice
* visual effect.
*/
if (currentBreakpoint === 1) {
targetOpacity = this.MAX_OPACITY;
}

// Re-enable transition for a smooth snap-back
modalEl.style.transition = '--backdrop-opacity 0.3s ease';
modalEl.style.setProperty('--backdrop-opacity', targetOpacity.toString());
}
}
```
Loading