Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
73 changes: 73 additions & 0 deletions docs/api/modal.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,26 @@ 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 `ionDragStart` and `ionDragEnd`

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 hiding certain interactive elements (like headers or buttons) to ensure a smooth dragging experience.

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`](#modaldrageventdetail) object. This event is commonly used to finalize state changes once the modal has come to a rest.

import DragStartEndEvents from '@site/static/usage/v8/modal/drag-start-end-events/index.md';

<DragStartEndEvents />

### Using `ionDragMove`

The `ionDragMove` event is emitted continuously while the user is actively dragging the modal. This event provides a [`ModalDragEventDetail`](#modaldrageventdetail) object containing real-time data, 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 a header's opacity as the modal is dragged upward.

import DragMoveEvent from '@site/static/usage/v8/modal/drag-move-event/index.md';

<DragMoveEvent />

## Interfaces

### ModalOptions
Expand Down Expand Up @@ -251,6 +271,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.
*/
snapBreakpoint?: number;
}
```

## Accessibility

### Keyboard Interactions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
```html
<ion-header #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
trigger="open-modal"
[initialBreakpoint]="0.25"
[breakpoints]="[0, 0.25, 0.5, 0.75, 1]"
(ionDragMove)="onDragMove($event)"
(ionDragEnd)="onDragEnd($event)"
(willDismiss)="onWillDismiss()"
>
<ng-template>
<ion-content class="ion-padding">
<div class="ion-margin-top">
<ion-label>Drag the handle to adjust the header's visibility.</ion-label>
</div>
</ion-content>
</ng-template>
</ion-modal>
</ion-content>
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
```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('header', { read: ElementRef })
header!: ElementRef<HTMLIonHeaderElement>;
// Assign the current snap breakpoint to the initial breakpoint so
// that we can track changes during the drag
currentSnap = 0.25;

onDragMove(event: CustomEvent<ModalDragEventDetail>) {
// `progress` is a value from 1 (top) to 0 (bottom)
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
const { progress, snapBreakpoint } = event.detail;

if (this.currentSnap !== snapBreakpoint) {
this.currentSnap = snapBreakpoint as number;

console.log('Current snap breakpoint:', snapBreakpoint);
}

const headerEl = this.header.nativeElement;
/**
* Inverse relationship:
* 1.0 progress = 0 opacity
* 0 progress = 1.0 opacity
*/
const currentOpacity = 1 - progress;

headerEl.style.opacity = currentOpacity.toString();
}

onDragEnd(event: CustomEvent<ModalDragEventDetail>) {
// `progress` is a value from 1 (top) to 0 (bottom)
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
const { progress, snapBreakpoint } = event.detail;
const headerEl = this.header.nativeElement;

/**
* If the modal is snapping to the closed state (0), reset the
* styles.
*/
if (snapBreakpoint === 0) {
headerEl.style.removeProperty('opacity');
headerEl.style.removeProperty('transition');
return;
}

// Smooth transition to the final resting opacity
headerEl.style.transition = 'opacity 0.4s ease';
// The final opacity matches the inverse of the resting progress
headerEl.style.opacity = (1 - progress).toString();
}

/**
* If the user dismisses the modal (e.g. tapping the backdrop),
* reset the styles.
*/
onWillDismiss() {
const headerEl = this.header.nativeElement;

// Reset styles when the modal is dismissed
headerEl.style.removeProperty('opacity');
headerEl.style.removeProperty('transition');
}
}
```
93 changes: 93 additions & 0 deletions static/usage/v8/modal/drag-move-event/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Modal</title>
<link rel="stylesheet" href="../../common.css" />
<script src="../../common.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/npm/@ionic/core@8/dist/ionic/ionic.esm.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@ionic/core@8/css/ionic.bundle.css" />
</head>

<body>
<ion-app>
<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 trigger="open-modal" initial-breakpoint="0.25">
<ion-content class="ion-padding">
<div class="ion-margin-top">
<ion-label>Drag the handle to adjust the header's visibility.</ion-label>
</div>
</ion-content>
</ion-modal>
</ion-content>
</ion-app>

<script>
const modal = document.querySelector('ion-modal');
const header = document.querySelector('ion-header');
// Assign the current snap breakpoint to the initial breakpoint so
// that we can track changes during the drag
let currentSnap = 0.25;
modal.breakpoints = [0, 0.25, 0.5, 0.75, 1];

modal.addEventListener('ionDragMove', (event) => {
// `progress` is a value from 1 (top) to 0 (bottom)
const { progress, snapBreakpoint } = event.detail;

if (currentSnap !== snapBreakpoint) {
currentSnap = snapBreakpoint;

console.log('Current snap breakpoint:', snapBreakpoint);
}

/**
* Inverse relationship:
* 1.0 progress = 0 opacity
* 0 progress = 1.0 opacity
*/
const currentOpacity = 1 - progress;

header.style.opacity = currentOpacity;
});

modal.addEventListener('ionDragEnd', (event) => {
// `progress` is a value from 1 (top) to 0 (bottom)
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
const { progress, snapBreakpoint } = event.detail;

/**
* If the modal is snapping to the closed state (0), reset the
* styles.
*/
if (snapBreakpoint === 0) {
header.style.removeProperty('opacity');
header.style.removeProperty('transition');
return;
}

// Smooth transition to the final resting opacity
header.style.transition = 'opacity 0.4s ease';
// The final opacity matches the inverse of the resting progress
header.style.opacity = 1 - progress;
});

/**
* If the user dismisses the modal (e.g. tapping the backdrop),
* reset the styles.
*/
modal.addEventListener('willDismiss', (event) => {
// Reset styles when the modal is dismissed
header.style.removeProperty('opacity');
header.style.removeProperty('transition');
});
</script>
</body>
</html>
29 changes: 29 additions & 0 deletions static/usage/v8/modal/drag-move-event/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Playground from '@site/src/components/global/Playground';

import javascript from './javascript.md';

import react from './react.md';

import vue from './vue.md';

import angular_example_component_html from './angular/example_component_html.md';
import angular_example_component_ts from './angular/example_component_ts.md';

<Playground
version="8"
code={{
javascript,
react,
vue,
angular: {
files: {
'src/app/example.component.html': angular_example_component_html,
'src/app/example.component.ts': angular_example_component_ts,
},
},
}}
src="usage/v8/modal/drag-move-event/demo.html"
devicePreview
includeIonContent={false}
showConsole={true}
/>
77 changes: 77 additions & 0 deletions static/usage/v8/modal/drag-move-event/javascript.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
```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 trigger="open-modal" initial-breakpoint="0.25">
<ion-content class="ion-padding">
<div class="ion-margin-top">
<ion-label>Drag the handle to adjust the header's visibility.</ion-label>
</div>
</ion-content>
</ion-modal>
</ion-content>

<script>
const modal = document.querySelector('ion-modal');
const header = document.querySelector('ion-header');
// Assign the current snap breakpoint to the initial breakpoint so
// that we can track changes during the drag
let currentSnap = 0.25;
modal.breakpoints = [0, 0.25, 0.5, 0.75, 1];

modal.addEventListener('ionDragMove', (event) => {
// `progress` is a value from 1 (top) to 0 (bottom)
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
const { progress, snapBreakpoint } = event.detail;

if (currentSnap !== snapBreakpoint) {
currentSnap = snapBreakpoint;

console.log('Current snap breakpoint:', snapBreakpoint);
}

/**
* Inverse relationship:
* 1.0 progress = 0 opacity
* 0 progress = 1.0 opacity
*/
const currentOpacity = 1 - progress;

header.style.opacity = currentOpacity;
});

modal.addEventListener('ionDragEnd', (event) => {
// `snapBreakpoint` tells us which snap point the modal will animate to after the drag ends
const { progress, snapBreakpoint } = event.detail;

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

// Smooth transition to the final resting opacity
header.style.transition = 'opacity 0.4s ease';
// The final opacity matches the inverse of the resting progress
header.style.opacity = 1 - progress;
});

/**
* If the user dismisses the modal (e.g. tapping the backdrop),
* reset the styles.
*/
modal.addEventListener('willDismiss', (event) => {
// Reset styles when the modal is dismissed
header.style.removeProperty('opacity');
header.style.removeProperty('transition');
});
</script>
```
Loading