React Native Modal with Backdrop (Deep-dive)

React Native Modal with Backdrop (Deep-dive)

We need to use React Native Modal in various scenarios in our application. The main purpose of the modal is to serve users an elevated view of any specific information and the actions associated with it. So, it plays a vital role but is often neglected the user experiences lies within it.

React Native provides a Modal component for general uses, but does it actually serve those purposes? Check out the example below from the official React Native official documentation.

The problem with the component is:

  • It doesn’t separate the component of the modal from the actual view. It just pops up on the screen, and it appears like a regular component. It actually blends with the actual screen and is hard to differentiate.

  • Sometimes the modal doesn’t have any button to close it. You can have a modal just to view information about something. But in this case, you must have a button that will close the modal. Clicking outside of the modal doesn’t close it.

Why doesn't the official Modal component have these functionalities? Because it provides a very simple implementation of how a modal works. Many of us don’t need these extra functionalities and don’t actually care about the user experience. But this is the blog for those who do CARE!

So you achieve this in various ways, and I’m going to show all the possible solutions and their drawbacks.

Using the react-native-modal package:

This package solves all the problems discussed above with the React Native default modal. It has the backdrop functionality, and it closes the modal down when a user clicks outside of the modal. Here's the code to showcase a modal using the package:

import { View, Text, SafeAreaView, Button } from "react-native";
import React, { useState } from "react";
import Modal from "react-native-modal";

type Props = {};

const Test2Screen = (props: Props) => {
  const [isModalVisible, setModalVisible] = useState(false);

  const toggleModal = () => {
    setModalVisible(!isModalVisible);
  };

  return (
    <SafeAreaView className="flex-1 justify-center items-center">
      <Button title="Show modal" onPress={toggleModal} />

      <Modal isVisible={isModalVisible} onBackdropPress={toggleModal}>
        <View className="bg-white rounded-xl p-6">
          <Text>Hello!</Text>
          <Button title="Hide modal" onPress={toggleModal} />
        </View>
      </Modal>
    </SafeAreaView>
  );
};

export default Test2Screen;

The code to achieve this modal is pretty straightforward, and there are literally no boilerplate codes for you to write since the package does it for you. But what if your company doesn't want you to use any external packages and wants you to code them yourself? Let's see if we can achieve the same features without using the external package.

Using the default React Native Modal:

React Native comes with a default modal out of the box, but it doesn't have any of the features we require. But can we tweak it? Yes, but to what length? Let's figure out.

First, add a backdrop. To create a backdrop, we need to understand what it means. It is kind of a shadow that covers the whole screen, and pressing it closes the modal. So we need a TouchableOpacity.

import {
  View,
  Button,
  SafeAreaView,
  Modal,
  TouchableOpacity,
} from "react-native";
import React, { useState } from "react";
import { StackScreenProps } from "@react-navigation/stack";

type Props = {};

const TestScreen = ({ navigation }: StackScreenProps<Props>) => {
  const [showModal, setShowModal] = useState(false);

  const toggleModal = () => {
    setShowModal(!showModal);
  };

  return (
    <SafeAreaView className="flex-1 justify-center items-center">
      <Modal
        animationType="fade"
        transparent={true}
        visible={showModal}
        onRequestClose={() => {
          setShowModal(false);
        }}
      >
        {/* Backdrop wrapping the modal content */}
        <TouchableOpacity
          onPressOut={() => {
            console.log("Click Outside");
            setShowModal(false);
          }}
          className="bg-black/50 h-screen justify-center items-center"
        >
          {/* Modal Content */}
          <View>
            <View className="bg-black rounded-xl w-48 h-48">
              <Button title="hide modal" onPress={() => setShowModal(false)} />
            </View>
          </View>
        </TouchableOpacity>
      </Modal>
      <Button title="Open Modal" onPress={toggleModal} />
    </SafeAreaView>
  );
};

export default TestScreen;

The difference here is that the modal content is popping with a 'fade' animation. We are using `animationType="fade"` here. What if we use animationType="slide" instead? Let's check:

In this case, a new problem arises. The backdrop is also sliding up along with the modal content. We don't want that; we want the backdrop to fade in and out and our modal content to slide up and down while toggling the modal. We can fix this behavior by removing the animation completely from the modal and assigning the animations separately to our modal content and backdrop. But it's a lot of work, and we can choose an easier way. If you are not concerned about the animations, you can just use fade-in and fade-out by using animationType="fade"`. But this arrives with another problem, which will be discussed and solved in the next section. For reference, we will call it ''the unknown problem''.

Using the react-native-reanimated package:

Excuse me? What? Are we using a package? But I thought you told me that we couldn't use a package. So what's wrong with the first package?

I know you are having these questions in your head, but here's why we are going to use the react-native-reanimated package instead of the react-native-modal package.

Reanimated Package

Modal Package

1. Officially recommended by React Native

1. Is not officially recommended by React Native

2. Can be used for various components, places, and use-cases.

2. Can only be used for specific components, places, and use-cases that are modal-related.

3. You need an animation library for your app since it will be used very frequently.

3. No. You don't need this. You can code this one component because you're awesome. Let's do this.

So we don't need any modal, right? We can code it ourselves. But where to start? Before starting, we need to know how this awesome animation library works. For now, just remember this line: ThisAnimated object wraps React Native built-ins such as View, ScrollView or FlatList.

Okay, now let's start. First, we need a backdrop that will cover the full screen, work as a shadow, clicking it will close the modal and the animation will be fade-in and fade-out. And lastly, we will need a modal content container that will slide up while entering and slide while exiting. To achieve these, let's introduce two Animated Views.

import {
  Button,
  TouchableOpacity,
  SafeAreaView,
  TouchableWithoutFeedback,
} from "react-native";
import React, { useState } from "react";
import Animated, {
  FadeIn,
  FadeOut,
  SlideInDown,
  SlideOutDown,
} from "react-native-reanimated";

type Props = {
  navigation: { openDrawer: () => void; navigate: (name: string) => void };
};

const HomeScreen = ({ navigation }: Props) => {
  const [showModal, setShowModal] = useState(false);

  const toggleModal = () => setShowModal(!showModal);

  return (
    <SafeAreaView className="flex-1">
      {showModal && (
        // Backdrop
        <Animated.View
          className="bg-black/50 absolute top-0 bottom-0 left-0 right-0 z-10"
          entering={FadeIn}
          exiting={FadeOut}
        >
          {/* Modal Content */}
          <Animated.View
            className="bg-black rounded-xl w-48 h-48"
            entering={SlideInDown}
            exiting={SlideOutDown}
          >
            <Button title="hide modal" onPress={toggleModal} />
          </Animated.View>
        </Animated.View>
      )}

      <Button title="show modal" onPress={toggleModal} />
    </SafeAreaView>
  );
};

export default HomeScreen;

And this is how it looks:

As you can see, the animations are working perfectly. What we did here is, we are rendering the parent View component (Backdrop) along with its children (content) conditionally. And, we are using here predefined exit and enter animations from Reanimated. Check the documentation for more about these animations, or you can simply make your own animation by following their official doc.

But if you notice, the Modal doesn't close if you click on the backdrop. Because here, we are not using any pressable/touchable elements. So, need to introduce a TouchableOpacity for the backdrop to register the touch on it.

return (
    <SafeAreaView className="flex-1 justify-center items-center">
      {showModal && (
        // Backdrop
        <Animated.View
          className="bg-black/50 absolute top-0 bottom-0 left-0 right-0 z-10"
          entering={FadeIn}
          exiting={FadeOut}
        >
          {/* Backdrop Pressing function */}
          <TouchableOpacity
            className="flex-1 justify-center items-center"
            onPress={toggleModal}
          >
            {/* Modal Content */}
            <Animated.View
              className="bg-black rounded-xl w-48 h-48"
              entering={SlideInDown}
              exiting={SlideOutDown}
            >
              <Button title="hide modal" onPress={toggleModal} />
            </Animated.View>
          </TouchableOpacity>
        </Animated.View>
      )}

      <Button title="show modal" onPress={toggleModal} />
    </SafeAreaView>
  );

This is the updated code. But is this enough? Let's see the demo:

Pressing on the backdrop closes the modal, hide modal button closes the modal and the animations are working perfectly. So what's the issue? The issue is, if you click on the modal content, it also closes the modal which was not supposed to be the case.This is ''the unknown problem'' we discussed previously. The outside modal press functionality is also working inside the modal and we need to stop this behavior. To stop this behavior, we need to stop triggering the outside press funtionality and to accomplish that, we need to wrap the modal content component inside a TouchableWithoutFeedback element so that it has its own event handler. So, the final code stands:

import {
  Button,
  TouchableOpacity,
  SafeAreaView,
  TouchableWithoutFeedback,
} from "react-native";
import React, { useState } from "react";
import Animated, {
  FadeIn,
  FadeOut,
  SlideInDown,
  SlideOutDown,
} from "react-native-reanimated";

type Props = {
  navigation: { openDrawer: () => void; navigate: (name: string) => void };
};

const HomeScreen = ({ navigation }: Props) => {
  const [showModal, setShowModal] = useState(false);

  const toggleModal = () => setShowModal(!showModal);

  return (
    <SafeAreaView className="flex-1 justify-center items-center">
      {showModal && (
        // Backdrop
        <Animated.View
          className="bg-black/50 absolute top-0 bottom-0 left-0 right-0 z-10"
          entering={FadeIn}
          exiting={FadeOut}
        >
          {/* Backdrop Pressing function */}
          <TouchableOpacity
            className="flex-1 justify-center items-center"
            onPress={(e) => {
              e.stopPropagation();
              toggleModal();
            }}
          >
            {/* Modal Content */}
            <TouchableWithoutFeedback>
              <Animated.View
                className="bg-black rounded-xl w-48 h-48"
                entering={SlideInDown}
                exiting={SlideOutDown}
              >
                <Button title="hide modal" onPress={toggleModal} />
              </Animated.View>
            </TouchableWithoutFeedback>
          </TouchableOpacity>
        </Animated.View>
      )}

      <Button title="show modal" onPress={toggleModal} />
    </SafeAreaView>
  );
};

export default HomeScreen;

Now everything is working as intended, and you just made yourself an awesome Modal for React Native. To finalize this, let's make this modal component modular.

Here's you modular BackdropModal component:

import { TouchableOpacity, TouchableWithoutFeedback } from "react-native";
import React, { ReactNode } from "react";
import Animated, {
  FadeIn,
  FadeOut,
  SlideInDown,
  SlideOutDown,
} from "react-native-reanimated";

type Props = {
  children: ReactNode;
  showModal: boolean;
  setShowModal: (val: boolean) => void;
};

const BackdropModal = (props: Props) => {
  const toggleModal = () => props.setShowModal(!props.showModal);

  return (
    <Animated.View
      className="bg-black/50 absolute top-0 bottom-0 left-0 right-0 z-10"
      entering={FadeIn}
      exiting={FadeOut}
    >
      {/* Backdrop Pressing function */}
      <TouchableOpacity
        className="flex-1 justify-center items-center"
        onPress={(e) => {
          e.stopPropagation();
          toggleModal();
        }}
      >
        {/* Modal Content */}
        <TouchableWithoutFeedback>
          <Animated.View entering={SlideInDown} exiting={SlideOutDown}>
            {props.children}
          </Animated.View>
        </TouchableWithoutFeedback>
      </TouchableOpacity>
    </Animated.View>
  );
};

export default BackdropModal;

And here's how to use it with other components/screens:

import { Button, SafeAreaView, Text, View } from "react-native";
import React, { useState } from "react";
import BackdropModal from "../components/modal/BackdropModal";

type Props = {
  navigation: { openDrawer: () => void; navigate: (name: string) => void };
};

const HomeScreen = ({ navigation }: Props) => {
  const [showModal, setShowModal] = useState(false);

  return (
    <SafeAreaView className="flex-1 justify-center items-center">
      {showModal && (
        <BackdropModal setShowModal={setShowModal} showModal={showModal}>
          <View className="bg-white rounded-xl p-4">
            <Text>Hello World!</Text>
          </View>
        </BackdropModal>
      )}

      <Button title="show modal" onPress={() => setShowModal(true)} />
    </SafeAreaView>
  );
};

export default HomeScreen;

I tried to discuss how you can build any component from the ground, and the core concept lies within it. I tried my best yet I might be wrong in some cases. Please comment below if you have any alternative or better approach, I will love it know about it.

Also checkout my portfolio site: https://www.sudwipto.com/

Thanks for reading this long post, I hope you learned something. Also, checkout the video where I code along while building this Modal component and why I used the tailwind classes above for a even better understanding where I described everything line by line. Chek out the video: https://youtu.be/Q4eIU2z7Ofo
Please give a like and subscribe to my channel. Peace ✌️

Complete Repo Link: https://github.com/sudwiptokm/react-native-modal

Portfolio: https://www.sudwipto.com/