Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f028c88
docs(modal): add playgrounds for sheet and card model drag events
thetaPC Feb 24, 2026
6950b0e
docs(modal): add drag events playground for card
thetaPC Feb 25, 2026
d38e734
docs(modal): add type import and cleanup
thetaPC Feb 25, 2026
867a38b
chore(modal): remove extra tag
thetaPC Feb 25, 2026
1befa48
chore(modal): update format
thetaPC Feb 25, 2026
8f9d11f
docs(modal): update playgrounds and add interface
thetaPC Feb 25, 2026
36e1b78
chore(stackblitz): add dev build
thetaPC Feb 25, 2026
2247d3f
docs(modal): update section titles
thetaPC Feb 26, 2026
bd55ca5
chore(stackblitz): update dev build
thetaPC Feb 26, 2026
8a5d2cb
docs(modal): create event handling section
thetaPC Feb 27, 2026
3fc476a
docs(modal): clarify deltaY
thetaPC Feb 27, 2026
351119a
docs(modal): change to predictedBreakpoint
thetaPC Feb 27, 2026
12620da
docs(modal): remove intro
thetaPC Mar 2, 2026
b8f8724
docs(modal): update heading levels
thetaPC Mar 2, 2026
9d5d012
docs(modal): update heading level
thetaPC Mar 2, 2026
db599b3
docs(modal): remove event list from dragMove
thetaPC Mar 2, 2026
ebcd579
docs(modal): separate playgrounds
thetaPC Mar 2, 2026
03828ff
docs(modal): add console logs
thetaPC Mar 2, 2026
656603c
feat(modal): cleanup
thetaPC Mar 3, 2026
dc144e0
docs(modal): update import name
thetaPC Mar 4, 2026
91069fd
docs(modal): move link
thetaPC Mar 4, 2026
7bb82f2
docs(modal): move link again
thetaPC Mar 4, 2026
f031a2c
docs(modal): use latest variable name
thetaPC Mar 4, 2026
db92545
docs(modal): update import path
thetaPC Mar 4, 2026
308d9b1
docs(modal): remove event from console
thetaPC Mar 4, 2026
0e2099e
docs(modal): remove event from the console end
thetaPC Mar 4, 2026
51a2ce8
docs(modal): add console for snap changes
thetaPC Mar 4, 2026
bfed59d
docs(modal): move events to top level modal usage directory
brandyscarney Mar 4, 2026
1b904f5
chore: revert dev build
brandyscarney Mar 4, 2026
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.
- `currentBreakpoint`: 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 `currentBreakpoint` 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 last event.
*
* 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.
*/
currentBreakpoint?: 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