Library / SDK
GSTJ/react-native-magic-modal avatar
GSTJ/react-native-magic-modal

react-native-magic-modal: imperative modals with typed results, from any async flow

A modal library that can be called imperatively from anywhere. Effortlessly control modals, streamline complex flows, and create a reliable user experience.

644 stars16 forksTypeScriptMIT

At a glance

What is it?
A TypeScript modal library for React Native, Expo, and web that lets you open a modal from any async function and await a typed result. It mounts one portal, keeps per-entry state, and distinguishes intentional hides from dismissals.
Who is it for?
Adopt react-native-magic-modal if you build React Native or Expo apps where modals must be opened from async flows (like confirmations after API calls) and you want typed results without wiring callbacks through props. Skip it if you need snap points or nested scrolling, or if you cannot add the native peers (react-native-gesture-handler, reanimated, worklets, screens) on iOS and Android.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 14, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem: modals trapped in JSX trees

In React Native, the standard way to show a modal is to render it conditionally in a component tree. That forces the caller to hold state, pass callbacks, and coordinate visibility across components. If a modal must appear after an async operation, like a network request or a timer, the logic spreads across several files. react-native-magic-modal solves this by letting you call magicModal.show() from anywhere, including inside a plain async function, and await the result. The README frames it as mounting one portal and opening a modal from any async flow. The target user is a React Native or Expo developer who wants a promise-based modal API instead of prop drilling and state flags.

How the portal and promise handle work

The library's core is a single portal component, MagicModalPortal, that owns a stack of modal entries. You mount it once near the root, inside GestureHandlerRootView for native apps. Each call to magicModal.show() pushes a new entry onto that stack. Each entry keeps its own component, configuration, ID, and promise. The show() call returns a handle that is itself the promise, with additional properties like modalID, update, and hide. The caller can await that handle and receive a HideReturn<T> object. That object contains the data passed to hide() and a reason field. The reason distinguishes an intentional hide from other dismissals, such as a backdrop press, a completed swipe, or a system action like Android back or web Escape. This design means a second show() call can open above the current modal without mixing their results, because each entry has its own promise.

Typed results and hide reasons

The library uses TypeScript generics to tie the result type across the caller and the modal content. You call magicModal.show<ConfirmationResult>(ConfirmationModal) and inside the modal you use useMagicModal<ConfirmationResult>() to get a hide function. When the modal calls hide({ confirmed: true }), the caller's await resolves with that data. The README shows a pattern where the caller checks result.reason against MagicModalHideReason.INTENTIONAL_HIDE before using the data. If the reason is not intentional, the caller can record a cancellation. This is a meaningful improvement over the usual onClose callback that gives no reason. The type system exposes data only after you narrow the result to INTENTIONAL_HIDE, which forces you to handle the dismissal case explicitly. The same result contract applies on web, where the modal renders as DOM elements instead of React Native views.

Installation and platform split

Installation differs by platform, which is a key detail. For Expo iOS and Android, you run pnpm add magic-modal and then npx expo install react-native-gesture-handler react-native-reanimated react-native-worklets react-native-screens. Those are the native peers. For Expo Web, the README says it bundles through Metro under the react-native condition, so it loads the native entry and takes the same peers as iOS and Android, plus react-dom and react-native-web. For Next.js and other browser-only React apps, the install is just pnpm add magic-modal. The web entry ships with zero React Native dependencies, so no bundler alias and no gesture or animation peers are needed. That split is practical: a browser app avoids the heavy native stack, but an Expo Web app does not, because Metro resolves the native entry. You must copy the Client Component setup from the Next.js guide, and a runnable example lives in examples/next-web.

Mounting the portal and closing from outside

For native and Expo, you mount MagicModalPortal inside GestureHandlerRootView. The README gives a minimal App component with that structure, and for Expo Router you put it in the root app/_layout.tsx. For a browser app, you mount the portal inside a Client Component and nothing else. To close a modal from outside its component, you keep the ID returned by show(). The README shows: const { modalID } = magicModal.show(StatusModal); magicModal.hide(undefined, { modalID }). This is useful when a modal is opened in one part of the app and must be dismissed from another, such as a global error handler. The library also provides hideAll() to close all entries, and each dismissal reason is distinct.

Limitations: no snap points, no nested scrolling

The FAQ is explicit about what the library does not do. It does not implement snap points or nested scrolling. If you need a bottom sheet with partial heights or a modal containing a ScrollView that also supports swipe-to-dismiss, you will hit a wall. The README suggests disabling swipe dismissal for a ScrollView by passing swipeDirection: undefined. That is a workaround, not a solution. The library also depends on react-native-gesture-handler and react-native-reanimated for animations and gestures on native platforms. If your project avoids those dependencies, this library forces them on you. The web entry avoids them, but only for browser-only apps. These constraints mean the library is not a drop-in replacement for a full-featured modal or bottom sheet library.

Alternatives and the trade-off in approach

The main alternative is a declarative modal library like @react-native-modal or react-native-modalize, which keep modals in the JSX tree and control visibility through props and state. The difference in approach is fundamental: those libraries require the caller to render a modal component and toggle a visible prop, whereas magic-modal inverts the flow with an imperative promise. That inversion is the entire sell. With a declarative library, you write more boilerplate but you keep all modal state in the component tree, which can be easier to reason about for simple cases. With magic-modal, you get a concise async/await pattern but you lose the visual locality of the modal in the component tree. For a simple confirmation dialog that appears after a button press, a declarative library may be simpler. For a modal that must appear from a deeply nested async function, the imperative approach is cleaner.

Maintenance, license, and what to verify

The repository is under the MIT license, which is permissive and suitable for commercial use. The last push was July 2026, with recent releases including 9.2.0, 10.1.1, and 10.2.0, so the project appears actively maintained. The README points to a documentation site, a FAQ, and a Medium article. There are runnable examples in examples/kitchen-sink and examples/next-web. Before adopting, verify the exact peer dependency versions for your React Native or Expo SDK, because the install commands use npx expo install to pick compatible versions. Also check the docs for Android back handling and iOS overlays, as those are platform-specific details that the README mentions but does not fully explain. The update() method on the handle suggests you can replace modal content, but the README does not show how, so read the advanced content replacement guide before relying on it.

Editorial conclusion

Adopt react-native-magic-modal if you build React Native or Expo apps where modals must be opened from async flows (like confirmations after API calls) and you want typed results without wiring callbacks through props. Skip it if you need snap points or nested scrolling, or if you cannot add the native peers (react-native-gesture-handler, reanimated, worklets, screens) on iOS and Android. The web-only entry is lighter, but verify your target platform's setup first: Expo Web uses the native entry with peers, while Next.js uses a zero-dependency browser entry. Check the docs for portal placement and Android back handling before committing.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
Community notes

Community notes