Magic Modallatest
Guides

Compose modal flows

Keep confirmation, response, and follow-up modals in one readable async function.

Because show returns a promise, a modal sequence reads like the interaction itself:

type Confirmation = { confirmed: boolean };

const confirmation = await magicModal.show<Confirmation>(ConfirmationModal, {
  accessibilityLabel: "Confirm changes",
});

if (
  confirmation.reason !== MagicModalHideReason.INTENTIONAL_HIDE ||
  !confirmation.data.confirmed
) {
  return;
}

const response = await saveChanges();

await magicModal.show(() => <ResultModal success={response.ok} />, {
  accessibilityLabel: response.ok ? "Changes saved" : "Save failed",
});

Cancellation is part of the result

Do not assume every close has data. A backdrop press or swipe resolves the promise with only a reason:

if (result.reason === MagicModalHideReason.INTENTIONAL_HIDE) {
  submit(result.data);
} else {
  trackCancellation(result.reason);
}

This discriminated union makes cancellation explicit and prevents accidental access to a value the user never submitted.

Stack another modal

Calling show while one modal is open pushes another entry. Await the inner modal before deciding what to do with the original:

const account = await magicModal.show<Account>(AccountPicker, {
  accessibilityLabel: "Choose an account",
});

if (account.reason === MagicModalHideReason.INTENTIONAL_HIDE) {
  await magicModal.show(() => <AccountDetails account={account.data} />, {
    accessibilityLabel: "Account details",
  });
}

Each call has its own modalID, promise, configuration, and hide lifecycle.

On this page