Introduction
Build awaitable, type-safe modal flows for web, iOS, and Android.
Magic Modal turns a modal interaction into a typed async result. Mount one
MagicModalPortal near the application root. Call show() from an event, effect, service, or any
other async flow.
const result = await magicModal.show<ConfirmationResult>(ConfirmationModal, {
accessibilityLabel: "Confirm purchase",
});
if (
result.reason === MagicModalHideReason.INTENTIONAL_HIDE &&
result.data.confirmed
) {
completePurchase();
}show() returns a handle that is itself the promise, carrying modalID, update, and hide for
the entry it opened. The older const { promise } = magicModal.show(...) form still works:
promise is an alias of the handle.
Core model
One root portal
MagicModalPortal owns the stack while each view keeps its normal component tree.
Typed close results
show<T>() and hide(data) connect the caller to submitted data and dismissal reasons.
Readable async flows
Await a modal, branch on its result, and open the next step in the same function.
Web, iOS, and Android
Use one API across web, iOS, and Android, with setup guides for Expo and Next.js.
The mental model
MagicModalPortalowns the modal stack. Mount it once.magicModal.showpushes one entry and returns an awaitable handle.useMagicModal().hidecloses that entry with typed data.- The promise resolves to
HideReturn<T>, including the close reason.
Every show() call creates an independent stack entry. Opening another modal keeps the earlier
entry and its promise in place.
Application modals
Magic Modal handles discrete interactions and async decisions. Snap points and nested bottom-sheet scrolling are outside its scope.
Set up a runtime
Choose the installation for the target. Mount the portal near the application root.