Magic Modallatest
Platforms

iOS and Android

Finish native setup and handle the behavior that differs between iOS and Android.

The portal, modal components, and typed results are shared. Native installation and system dismissals have platform-specific setup.

Native installation

Expo projects should use the Expo guide. In a bare React Native project, install every peer:

pnpm add magic-modal react-native-gesture-handler react-native-reanimated react-native-worklets react-native-screens

Add the Worklets plugin last:

babel.config.js
module.exports = function (api) {
  api.cache(true);

  return {
    presets: ["module:@react-native/babel-preset"],
    plugins: [
      // Existing plugins
      "react-native-worklets/plugin",
    ],
  };
};

Install iOS pods after changing native dependencies:

npx pod-install

Magic Modal 9 uses Reanimated 4 and requires React Native's New Architecture.

Platform behavior

BehavioriOSAndroid
Backdrop pressResolves with BACKDROP_PRESSResolves with BACKDROP_PRESS
Swipe dismissalResolves with SWIPE_COMPLETEResolves with SWIPE_COMPLETE
System back actionNo matching system actionResolves with BACK_BUTTON_PRESS
Accessibility escapeResolves with BACK_BUTTON_PRESSResolves with BACK_BUTTON_PRESS
Full-window overlayEnabled by default through ScreensNo-op
Intentional closeResolves with data on both platformsResolves with data on both platforms

Android back action

The active modal consumes the Android back action and closes by default. Handle the close reason in the caller:

const result = await magicModal.show<CheckoutResult>(CheckoutModal);

switch (result.reason) {
  case MagicModalHideReason.INTENTIONAL_HIDE:
    return submitCheckout(result.data);
  case MagicModalHideReason.BACK_BUTTON_PRESS:
    return saveCheckoutDraft();
  case MagicModalHideReason.BACKDROP_PRESS:
  case MagicModalHideReason.SWIPE_COMPLETE:
  case MagicModalHideReason.GLOBAL_HIDE_ALL:
    return;
}

Override onBackButtonPress when the modal needs a guard before closing:

magicModal.show(DestructiveActionModal, {
  onBackButtonPress: ({ hide }) => {
    if (canLeaveDestructiveAction()) {
      hide({ reason: MagicModalHideReason.BACK_BUTTON_PRESS });
    }
  },
});

An override controls whether and when hide runs. Leaving the callback without calling hide keeps the modal open.

iOS full-window overlay

The portal uses react-native-screens to render above native modal screens on iOS. The overlay is enabled by default.

Temporarily disable it when a native picker should cover application modals:

magicModal.disableFullWindowOverlay();

try {
  await openImagePicker();
} finally {
  magicModal.enableFullWindowOverlay();
}

Both functions are no-ops on Android and web.

Backdrop and swipe policy

The default backdrop closes the top modal. Override it when a flow requires a guard:

magicModal.show(UnsavedFormModal, {
  onBackdropPress: ({ hide }) => {
    if (canDiscardDraft()) {
      hide({ reason: MagicModalHideReason.BACKDROP_PRESS });
    }
  },
});

Swipe dismissal defaults to "down". Disable it for forms and lists where the gesture competes with scrolling:

magicModal.show(ScrollableFormModal, {
  swipeDirection: undefined,
});

Safe areas and keyboards

Magic Modal positions the overlay. Modal content still owns its safe-area padding, keyboard avoidance, maximum height, and scrolling. Compose the same React Native primitives already used by the application, such as SafeAreaView, KeyboardAvoidingView, and ScrollView.

Keep long forms below the device height and disable modal swipe when the form itself scrolls.

On this page