ui > Modal

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.

Clicks: 0
OPEN MODAL
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
<div id="app-modal"/>

Full Example Code

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;
height: 60px;
display: flex;
justify-content: center;
`;
export const ConfirmButton = styled.div`
margin: 10px;
color: white;
height: 40px;
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;
`;
export const MainButton = styled.button`
`;
function ModalContainer({ setOpen, data, setData }) {
const [localData, setLocalData] = useState(data);
const { clicks } = localData;
function close() {
setOpen(false);
}
function submit() {
setData({
clicks,
});
close();
}
const content = new Array(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'),
);
}
export function ModalExample(props) {
const [open, setOpen] = useState(false);
const [data, setData] = useState({ clicks: 0 });
return (
<div>
<div>Clicks: {data.clicks}</div>
<MainButton
onClick={() => {
setOpen(true);
}}
>
OPEN MODAL
</MainButton>
{open && (
<ModalContainer
{...props}
setOpen={setOpen}
data={data}
setData={setData}
/>
)}
</div>
);
}

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 MainButton styled-component (we used our own style in this example), 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) do 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.

const Modal = styled.div`
// Need to specify a max width
max-width: 500px;
background-color: white;
// What allows us to position relative to the viewport
position: fixed;
// Some padding from the top of the browser
top: 75px;
// Arbitrary value so it covers other z-index components
z-index: 5;
// Always maintains height of browser minus 200px
max-height: calc(100% - 200px);
// Always stays centered (250px is half of 500px)
left: calc(50% - 250px);
// It's a column flexbox so that we can ensure the footer is locked at the bottom
display: flex;
flex-direction: column;
// On mobile, we don't need to center so set left to 0px
// Also add a slight margin on the left and right
@media (max-width: 500px) {
left: 0px;
margin: 0px 10px;
}
`;

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.

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

Scrolling Modal Content and Locked Modal Footer

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

Clicks: 0
OPEN MODAL

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.

export const ModalContent = styled.div`
// allows scrolling inside the content
overflow: auto;
min-height: 200px;
padding: 0px 40px;
padding-bottom: 80px;
`;

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

export const ModalFooter = styled.div`
// shadow above the footer element
box-shadow: 0px -2px 10px 0px grey;
height: 60px;
// Make a flexbox to put the confirm button in the center
display: flex;
justify-content: center;
`;

Still stuck?

Weren't able to find what you need? Book a free call with a React School developer today.

🛈 React School creates templates and video courses for building beautiful apps with React. Download our free Material UI template.