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.
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 /> </> ); }