Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 67 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
<h1 align="center">React Native Magic Modal 🦄</h1>
<h1 align="center">React Native Magic Modal</h1>

<p align="center">Call <code>show()</code> from any async flow. Await a typed result that records how the modal closed.</p>

<a href="./media/magic-modal-demo.mp4">
<img
alt="A rating flow moving from magicModal.show to a typed close result"
src="./media/magic-modal-demo.gif"
/>
</a>

<p align="center"><a href="./media/magic-modal-demo.mp4">Watch the MP4</a></p>

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/50031755/182908700-6b94a470-0e5c-4af2-acba-b20acaffa0b8.png">
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/50031755/182908700-6b94a470-0e5c-4af2-acba-b20acaffa0b8.png">
<img alt="React Native Magic Modal Banner" src="https://user-images.githubusercontent.com/50031755/182908700-6b94a470-0e5c-4af2-acba-b20acaffa0b8.png">
</picture>
<p align="center">
<a aria-label="NPM Version" href="https://www.npmjs.com/package/react-native-magic-modal">
<img alt="" src="https://img.shields.io/npm/v/react-native-magic-modal.svg?label=NPM&logo=npm&style=for-the-badge&color=0470FF&logoColor=white">
Expand All @@ -21,33 +27,27 @@
</p>

> [!NOTE]
> Magic Modal is a headless orchestration primitive: show modal content from anywhere, await a typed result, and keep styling in your own components.
> Magic Modal owns the flow. Your components own the UI. Open a modal from anywhere and await its typed result.

> [!TIP]
> The complete guides and API reference live in the [documentation](https://gstj.github.io/react-native-magic-modal/docs/).

## Features

- 📲 [**Easy Integration**](#quickstart): Seamlessly integrate with your React Native app.
- 🔄 [**Complex Flow Management**](#examples): Manage intricate modal sequences effortlessly.
- 🔧 [**Customizable**](#usage): Tailor modals to fit your app's unique requirements.

## Highlights
> Guides and API reference live in the [documentation](https://gstj.github.io/react-native-magic-modal/docs/).

React Native Magic Modal offers a superior experience compared to traditional modal implementations:
## Core API

- 🎨 [**Bring Your Own UI**](#examples): Style ordinary React Native content for iOS and Android.
- 🚀 [**Developer Friendly**](#quickstart): Simple to use, with a focus on developer experience.
- 🧩 [**Versatile**](#documentation): Adaptable to a wide range of modal scenarios.
- `magicModal.show()` renders content in the portal and returns an entry handle with a promise.
- `modalID` targets one entry for an update or close.
- `update()` replaces the component rendered by an open entry.
- `HideReturn<T>` records how the modal closed and carries submitted data.
- Modal components remain ordinary React Native UI.

## Table of Contents

- [**Installation**](#installation)
- [**Quickstart**](#quickstart)
- [**Examples**](#examples)
- [**Documentation**](#documentation)
- [**FAQ**](#faq)
- [**Contributors**](#contributors)
- [Installation](#installation)
- [Quickstart](#quickstart)
- [Try it](#try-it)
- [Documentation](#documentation)
- [FAQ](#faq)
- [Contributors](#contributors)

## Installation

Expand All @@ -70,18 +70,18 @@ Minimum peer versions:

Both gesture-handler majors work. Swipe-to-dismiss uses 3.x's `usePanGesture` hook when it's available and falls back to 2.x's `Gesture.Pan()` builder otherwise.

Reanimated 4 requires React Native's New Architecture. In a bare React Native app, add `"react-native-worklets/plugin"` last in `babel.config.js`, then run `npx pod-install`. Expo configures the plugin through its Babel preset; install compatible native versions with:
Reanimated 4 requires React Native's New Architecture. In a bare React Native app, add `"react-native-worklets/plugin"` last in `babel.config.js`. Run `npx pod-install`. Expo configures the plugin through its Babel preset; install compatible native versions with:

```bash
npx expo install react-native-gesture-handler react-native-reanimated react-native-worklets react-native-screens
pnpm add react-native-magic-modal
```

8.0.0 was the one version that required gesture-handler 3.x. If you're on it and pinned to 2.x, upgrade to 9.0.0 or later, and drop `react-native-gesture-handler` from `expo.install.exclude` if you added it to quiet the version check.
Only 8.0.0 required gesture-handler 3.x. If you're on it and pinned to 2.x, upgrade to 9.0.0 or later, and drop `react-native-gesture-handler` from `expo.install.exclude` if you added it to quiet the version check.

## Quickstart

Insert a `MagicModalPortal` at the top of your application structure, and a `GestureHandlerRootView` if you haven't already:
Mount a `MagicModalPortal` at the app root, and add a `GestureHandlerRootView` if you haven't already:

```tsx
import { MagicModalPortal } from "react-native-magic-modal";
Expand All @@ -97,21 +97,22 @@ export default function App() {
}
```

Tip: the root `_layout.tsx` is usually the best place to put it in a project using expo-router.
In Expo Router, mount it in the root `_layout.tsx`.

The `GestureHandlerRootView` is required. The portal renders a `GestureDetector` for the swipe gesture, and gesture-handler 3.x throws when one renders without a root view above it. 2.x only logs a warning.

## Examples
## Try it

Showcasing modal management on iOS and Android platforms:
The [interactive docs](https://gstj.github.io/react-native-magic-modal/) run the
package directly in the browser. Try the mobile rating flow, stack a second
caller, or watch `update()` drive a web upload.

| iOS | Android |
| ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| <img src="https://github.com/GSTJ/react-native-magic-modal/assets/50031755/fe95c4d9-3af5-4441-a36c-74dcb56ae78c" height=600/> | <img src="https://github.com/GSTJ/react-native-magic-modal/assets/50031755/f9effb46-7b5e-4371-a797-a84efb537346" height=600/> |
The [kitchen-sink app](examples/kitchen-sink) covers the native iOS and Android
paths.

## Usage

Here's the preferred usage pattern for the library:
Start with a modal that returns data to its caller:

```tsx
import React from "react";
Expand All @@ -128,7 +129,7 @@ const ConfirmationModal = () => {
return (
<View>
<TouchableOpacity onPress={() => hide({ success: true })}>
<Text>Click here to confirm</Text>
<Text>Confirm</Text>
</TouchableOpacity>
</View>
);
Expand All @@ -148,41 +149,42 @@ const ResponseModal = ({ text }) => {
};

const handleConfirmationFlow = async () => {
// You can call `show` with or without props, depending on the requirements of the modal.
// The render callback can pass props to the modal.
const result = await magicModal.show<ConfirmationModalReturn>(() => <ConfirmationModal />)
.promise;

// Hide could potentially be a backdrop press, a back button press, or a swipe gesture.
// Non-intentional closes include backdrop presses, Android back presses, and swipes.
if (result.reason !== MagicModalHideReason.INTENTIONAL_HIDE) {
// User cancelled the flow
return;
}

if (result.data.success) {
return magicModal.show(() => <ResponseModal text="Success!" />).promise;
return magicModal.show(() => <ResponseModal text="Confirmed." />).promise;
}

return magicModal.show(() => <ResponseModal text="Failure :(" />).promise;
return magicModal.show(() => <ResponseModal text="Confirmation failed." />).promise;
};

export const MainScreen = () => {
return (
<TouchableOpacity onPress={handleConfirmationFlow}>
<Text>Start the modal flow!</Text>
<Text>Start confirmation flow</Text>
</TouchableOpacity>
);
};
```

You can also hide modals imperatively outside of the modal context. For that, we provide the global `hide` method, that requires a modal id:
You can also close a modal outside its component. Pass its `modalID` to the
global `hide` method:

```tsx
import { magicModal } from "react-native-magic-modal";

const QuickModal = ({ text }) => {
return (
<View>
<Text>Hey! I'm going to be closed imperatively</Text>
<Text>The caller will close this modal.</Text>
</View>
);
};
Expand All @@ -193,8 +195,8 @@ const handleQuickModal = async () => {
// Wait for 2 seconds before closing the modal
await new Promise((resolve) => setTimeout(resolve, 2000));

// Note that it's usually preferable to use the `hide` method from the modal context
// You can even put it inside useEffects to handle auto-dismissal for you.
// Prefer hide() from the modal context when the modal owns the close action.
// Call it from an effect to auto-dismiss the modal.
magicModal.hide(undefined, { modalID });
};

Expand All @@ -207,7 +209,7 @@ export const MainScreen = () => {
};
```

`show` also hands you an `update` function, for when the data driving the modal lives outside of it:
`show` also returns `update()` for data that lives outside the modal:

```tsx
import { magicModal } from "react-native-magic-modal";
Expand All @@ -229,43 +231,49 @@ const handleUpload = async (file) => {
};
```

The modal itself stays put: same position in the stack, same backdrop, and the promise from `show` keeps waiting. Only the content is swapped, and it's a new component, so it mounts from scratch and anything it kept in `useState` is gone. If the state belongs to the modal, drive it from inside with a store or context instead.
The entry keeps its stack position, backdrop, and pending promise. Only the
component changes. It mounts from scratch, so local `useState` resets. Keep
modal-owned state in a store or context when it must survive an update.

Refer to the [kitchen-sink example](examples/kitchen-sink) for detailed usage scenarios.
See the [kitchen-sink example](examples/kitchen-sink) for runnable iOS and Android flows.

## Documentation

Access the complete documentation [here](https://gstj.github.io/react-native-magic-modal/).
Read the [setup, guides, and API reference](https://gstj.github.io/react-native-magic-modal/).

## FAQ

**Q:** Can I have two modals showing up at the same time?
**Q:** Can two modals be open at once?

**A:** Yes. With v4+, you can now have multiple modals showing up at the same time.
**A:** Yes. Every `show()` call adds an independent stack entry with its own ID
and promise.

---

**Q:** Can I use Scrollables inside the modal?
**Q:** Can I put scrollable content inside a modal?

**A:**
Yes, but Scrollables can't be used with swipe gestures enabled, as they conflict. Pass in `swipeDirection: undefined` on the `magicModal.show` function to disable gestures on them.
Yes, but the scroll gesture conflicts with swipe dismissal. Pass
`swipeDirection: undefined` to `magicModal.show()` for a scrollable modal.

If your use-case is a scrollable bottom-sheet, I recommend going with Gorhom's react-native-bottom-sheet for this use-case temporarily.
For a full bottom-sheet component, use
[React Native Bottom Sheet](https://github.com/gorhom/react-native-bottom-sheet).

---

**Q:** Modals are appearing on top of native modal screens, such as the image picker. How can I fix this?

**A:**
This behavior can be disabled by calling `magicModal.disableFullWindowOverlay()` before showing the modal. This will prevent the modal from appearing on top of native modal screens.
Call `magicModal.disableFullWindowOverlay()` before opening the modal.

You can also call `magicModal.enableFullWindowOverlay()` to re-enable it.
Call `magicModal.enableFullWindowOverlay()` when the overlay should cover native
screens again.

## Contributors

Special thanks to everyone who contributed to making React Native Magic Modal a robust and user-friendly library. [See the full list](https://github.com/GSTJ/react-native-magic-modal/graphs/contributors).
[See everyone who has contributed](https://github.com/GSTJ/react-native-magic-modal/graphs/contributors).

See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository.
See the [contributing guide](CONTRIBUTING.md).

## License

Expand Down
Loading
Loading