Skip to main content

Modals

What a modal is, how to show one, the presentation styles, sheets with detents, popovers, how a modal closes and how to ask before it does.

SectionModalsDraftDocumented, not reviewed yet: the API can change.

A modal is a task of its own over the app: writing a message, a form, the sign-in, a picker. The user finishes it or cancels it, and the app is as it was. showModal shows one, and dismiss closes it:

src/screens/Inbox.tsx
import { Button } from 'react-native';
import { useNavigation } from '@apexnative/navigation';

function Inbox() {
const navigation = useNavigation();
return (
<Button
title="Write"
onPress={() => navigation.showModal('Compose')}
/>
);
}

Compose comes up from the bottom over Inbox: a sheet on iOS, the whole screen on Android. A swipe down on iOS, or the system back on Android, closes it.

This page answers:

  1. What a modal is, and how it differs from a push.
  2. How to show one.
  3. How it looks: a sheet, full screen, a form, a popover.
  4. Sheets that stop at half and full height.
  5. How a modal closes, and how to ask before it does.

What a modal is​

A push continues the way the user is going; a modal interrupts it for a moment. That is why a modal shows over everything, tabs and bars too, and why it ends as a whole: closing it closes all its screens at once.

iOSA sheet by default: it rises from the bottom, and the screen under it steps back. Full screen, detents or a popover on iPad with its settings.
AndroidRises from the bottom and covers the screen.
BackA swipe down on an iOS sheet, the system back on Android, the close button, dismiss().
  • It has a stack of its own, so its screens can push: a form with several steps is one modal. With stack: false the screen shows alone, without a top bar.
  • The screen under it stays mounted. Under a full-screen modal it is out of sight. Under a sheet it still shows, so it doesn't disappear at all. See The life of a screen.
  • Modals go one over another. A modal can show another; each closes on its own, or all at once.

Showing a modal​

showModal takes the screen, then its props and the settings of the modal in one object:

navigation.showModal('Compose', {
props: { replyTo: messageId },
closeButton: true,
});

Compose shows with a close button at the leading edge of its top bar. The settings:

SettingWhat it does
closeButtonA close button on the first screen: the system one on iOS, an icon on Android. Needs the stack. A new first screen from setStack or replace in the modal's stack gets it too.
onDismissCalled once the modal is gone, whatever closed it: a swipe, a command, a new root. setStack and replace in the modal's stack replace its first screen and call it too.
stackfalse: the screen alone, without a stack and its top bar.
style, sheet, popover, contentSizeHow it looks: see How a modal looks.
transition, optionsThe animation, and the screen's options for this show.

presentation.modal of createNavigation sets these for every modal of the app, such as a close button everywhere: see Defaults for the whole app.

How a modal looks​

style picks how the platform presents the modal:

iOS
iPadOS
Android
navigation.showModal('Compose', { style: 'fullScreen' });

Without style, a modal is a sheet on iOS and covers the whole screen on Android.

'pageSheet' and 'formSheet' are sheets on both platforms: on Android the Material bottom sheet over the dimmed screen, at the full height under the status bar and at most 640 dp wide. 'fullScreen' and 'overFullScreen' come up from the bottom over the whole screen; on Android with the system's motion of a window opening over another, without a fade.

'overFullScreen' keeps the screen under the modal in place, so it shows through wherever the modal draws nothing: a dialog of your own, a card over a dimmed app. Give the modal's screen backgroundColor('transparent') in its options, so its own background doesn't hide the screen under it.

A form on iPad​

iPadOS A form sheet is a card in the middle of the screen. contentSize sets its width and height, or a height that follows the content:

navigation.showModal('EditProfile', {
style: 'formSheet',
contentSize: { width: 540, height: 'content' },
});

On iPhone and on Android a form sheet is a sheet, and contentSize does nothing.

A popover​

iPadOS A popover points at what opened it: a button of the bar by its id (sourceButtonId), or a view by its nativeID (sourceNativeID):

navigation.showModal('Filters', {
popover: { sourceButtonId: 'filter', arrowDirections: ['up'] },
});

On iPhone it becomes a sheet, unless adaptive: false keeps it a popover. Android has no popovers: the modal is the bottom sheet of 'pageSheet'.

iPadOS
iOS

Sheets​

A sheet can stop at several heights, its detents, as the sheets of the system apps do. The user drags it between them:

iOS
Android
navigation.showModal('Places', {
sheet: { detents: ['medium', 'large'] },
});

Places opens at half the height, the smallest detent, and the user drags it to the full height. A detent is 'medium', 'large', a height in points, or 'content', the height of the screen's content. The other keys of sheet:

  • selectedDetent: the detent the sheet opens at.
  • largestUndimmedDetent: up to this detent the screen under the sheet isn't dimmed and takes touches, as under the sheet of Maps.
  • grabber: the handle at the top.
  • expandsOnScroll: false: only the top drags the sheet, so a list inside it scrolls first.

Each detent the user drags to sends sheetDetentChanged. The screen moves the sheet itself by changing its options, and the sheet follows with an animation:

navigation.setOptions({
modal: { sheet: { selectedDetent: 'large' } },
});

On Android a sheet is the Material bottom sheet. It stops at half ('medium') and full ('large') height and at heights of its own; 'content' stops at half.

Android 14+ The back gesture over a sheet shows it sliding down before the user lets go: the preview of a sheet closing.

Sheets in a real app, step by step: Bottom sheets.

How a modal closes​

WhoHow
The userA swipe down on a sheet, the system back on Android, the close button.
The modal itselfdismiss() from any of its screens closes the whole modal.
Other codenavigation.dismissModal(id) by the id showModal resolved with; dismissAllModals() closes every one.
iOS
Android

Most often a modal closes itself once its task is done: the message is sent, the form is saved. Here a modal to write a message closes after Send:

src/screens/Compose.tsx
import { useState } from 'react';
import { Button, TextInput, View } from 'react-native';
import { useNavigation } from '@apexnative/navigation';

function Compose() {
const navigation = useNavigation();
const [text, setText] = useState('');

const send = async () => {
await sendMessage(text); // your own request
await navigation.dismiss(); // the whole modal closes
};

return (
<View>
<TextInput value={text} onChangeText={setText} />
<Button title="Send" onPress={send} />
</View>
);
}

In a modal of several steps, dismiss() on the last step closes them all at once: the user is back where they opened the modal.

Whatever closed it, onDismiss of showModal runs, and the event modalDismissed follows. onDismiss is the place to update the screen that opened the modal:

navigation.showModal('Compose', {
onDismiss: () => refresh(), // your own step
});

A new root closes the modals too: see What a switch does.

Asking before closing​

A modal with unsaved changes shouldn't close on a careless swipe. usePreventLeave keeps it on screen while its condition is true. The swipe down, the close button and the system back on Android call its handler instead, so the screen can ask:

import { Alert } from 'react-native';
import { usePreventLeave } from '@apexnative/navigation';

// in Compose, after its state
usePreventLeave(text.length > 0, () => {
Alert.alert('Discard the draft?', undefined, [
{ text: 'Keep editing' },
{ text: 'Discard', onPress: () => navigation.dismiss() },
]);
});

While the draft is empty, the modal closes as any other. Commands aren't held, so dismiss() in Discard closes it. The same hook holds the back of a stack: see Keeping the user on a screen.

A form in a modal, step by step: Forms in a modal.

API​