React Modal Tutorial with Portals

In this tutorial we're going to build a Modal popup component rendered through the use of React portals. A portal allows you to render a component outside of the current parent/child hierarchy. Because you always want your Modal to appear "on top" of all your other React components, a portal is the perfect use case to "teleport" rendering there.

In addition to building and rendering the Modal through the portal, we're also going to show a very common use-case of setting and retrieving data from this Modal. Try out the below example to observe this behavior.

Below you'll find the finished code for the example above. If you want to add this to your app, you need to add the portal element itself to the top of your DOM. That looks like:

  // Somewhere at the top of your App that is rendered once
  <div id="app-modal"/>

Using Hooks with React Modals

You can see that we kick off the example with some useState hooks.

  const [open, setOpen] = useState(false);
  const [data, setData] = useState({ clicks: 0 });

Our goal is to track the state of the modal (open or closed) and then the click data (initialized to an object with a clicks attribute). Typically you will want to store other data in this object because you're tracking more than one attribute, so that's why we used an object instead of a single number.

We are then logging the clicks with: <div>Clicks: {data.clicks}</div>.

Next we create a button to open the Modal. We use our setOpen setter to set the open state to true (it was default false).

This is now the boolean signal to mount the ModalContainer. This doesn't get mounted because expressions of the form expression && (something) does not evaluate the (something) unless the expression is true. So that's how we conditionally render the Modal with React hooks.

React Modal Main CSS Walkthrough

The main Modal element is a box that sits in the center of the browser. The position: fixed CSS sets the Modal to be rendered relative to the browser's viewport. We then use top & left to center the modal. We ensure that the modal never grows larger than the browser's height by setting max-height: calc(100% - 100px). We set the Modal to be a flexbox so that we can allow scrollable content in the center with the overflow: auto attribute (see ModalContent below). If the element was not a flexbox, the ModalFooter would not stick to the bottom of the Modal when the height of the Modal is decreased.

The Modal Shadow

This is another fixed element that stretches the entire browser's viewport. By setting a black background and an opacity of 70%, we create a translucent shadow on the rest of our content so that the Modal becomes clearly visible.

Scrolling Modal Content and Locked Modal Footer

The example here demonstrates the scrolling content in the center of the Modal by adding a bunch of extra text.

The overflow: auto on the ModalContent causes a scrollable container to be created, while letting the ModalFooter to be kept in view as to appear stuck to the bottom.

We set the ModalFooter to also be a flexbox so that we can center the button inside.

Menu
Lesson 4/7
import React, { useState } from "react";
import ReactDOM from "react-dom";
import styled from "styled-components";
const Modal = styled.div`
  max-width: 500px;
  background-color: white;
  position: fixed;
  top: 75px;
  z-index: 5;
  max-height: calc(100% - 200px);
  left: calc(50% - 250px);
  display: flex;
  flex-direction: column;
  @media (max-width: 500px) {
    left: 0px;
    margin: 0px 10px;
  }
`;

export const ModalContent = styled.div`
  overflow: auto;
  min-height: 200px;
  padding: 0px 40px;
  padding-bottom: 80px;
`;

export const ModalFooter = styled.div`
  box-shadow: 0px -2px 10px 0px grey;
  display: flex;
  padding: 8px; 
  justify-content: center;
`;

export const ConfirmButton = styled.button`
  padding: 4px;
  color: white;
  height: 40px;
  font-size: 16px; 
  text-transform: uppercase; 
  border: 0px; 
  align-self: center; 
  border-radius: 5px;
  padding: 5px;
  text-align: center;
  width: 200px;
  cursor: pointer;
  background-color: blue;
`;

const ModalShadow = styled.div`
  position: fixed;
  height: 100%;
  width: 100%;
  top: 0px;
  background-color: black;
  opacity: 0.7;
  z-index: 4;
`;

const ModalBanner = styled.div`
  margin-bottom: 20px;
  background-color: blue;
  color: white;
  padding: 10px;
`;

const Input = styled.input`
  text-align: right;
  width: 200px;
  margin-left: 15px;
`;

function ModalContainer({ setOpen, data, setData, tall }) {
  const [localData, setLocalData] = useState(data);
  const { clicks } = localData;
  function close() {
    setOpen(false);
  }
  function submit() {
    setData({
      clicks,
    });
    close();
  }
  const content = new Array(tall ? 15 : 1).fill(
    <p>
      Edit the clicks below by clicking on the number input or typing in your
      own value.
    </p>
  );
  return ReactDOM.createPortal(
    <>
      <ModalShadow onClick={close} />
      <Modal>
        <ModalBanner>Edit Clicks</ModalBanner>
        <ModalContent>
          {content}
          <label>
            Clicks
            <Input
              value={clicks}
              type="number"
              onChange={(e) => setLocalData({ clicks: e.target.value })}
            />
          </label>
        </ModalContent>
        <ModalFooter>
          <ConfirmButton onClick={submit}> Submit </ConfirmButton>
        </ModalFooter>
      </Modal>
    </>,
    document.getElementById("app-modal")
  );
}

function ModalExample(props) {
  const [open, setOpen] = useState(false);
  const [data, setData] = useState({ clicks: 0 });
  return (
    <div>
      <div>Clicks: {data.clicks}</div>
      <button
        onClick={() => {
          setOpen(true);
        }}
      >
        OPEN MODAL
      </button>
      {open && (
        <ModalContainer
          {...props}
          setOpen={setOpen}
          data={data}
          setData={setData}
        />
      )}
    </div>
  );
}

export default function App() {
  return (
    <>
      <div id="app-modal" />
      <h1> Modal example with hooks </h1>
      <ModalExample />
      <h1> Modal with scrollable text </h1>
      <ModalExample tall />
    </>
  );
}