DEV Community

Linas Spukas
Linas Spukas

Posted on


React Portals: Flexible Modal Implementation

When it comes to handling modals, dialogues, tooltips or hover cards, your best friend is React Portal. In short, it does what it's named for, ports a component to the specified location. A location, or better to call a container, can be any element in the DOM, even outside the React root component. That's why it is magic.

The portal is defined in react-dom library as a named export and has the following syntax:

import { createPortal } from 'react-dom';

createPortal(child, container);

A child represents the content you wish to render. It can be any HTML element, string or React Fragment. A container must be a target DOM element where the content will be rendered.

The most common use case for portals is modals. Normally React renders a returned element as a child to its closest parent component. This starts to become a problem when a parent component has styles such as a relative position, z-index or hidden overflow. This prevents child element to break out from the parent element boundaries. As the desired behaviour of modals or dialogs is to be rendered on top of other elements, portals provide an elegant way to render children outside the React tree and escape any style restrictions. The following example illustrates how the DOM elements are rendered when using a portal:

import React from "react";
import { createPortal } from 'react-dom';

export default function App() {
  return (
    <div className="parent">
      {createPortal(<div className="child">Child content</div>, document.body)}

This will yield a little bit unussual DOM structure, where the child component is mounted outside the parent:

  <div id="root">
    <div class="parent"></div>

  <div class="child">Child content</div>

But what is more fascinating, is if you would open React DevTools, you could see, that the rendered element is a direct child component of the App parent component. It means, that the child component, even if rendered outside the DOM hierarchy of the parent component, has the access to context, props, state and handlers of the parent element. And all events, fired from the inside of portal will bubble up to its ancestor. This makes the handling of modal, dialogs and tooltips more flexible.

Example of Practical Implementation

import React, { useEffect, useState } from "react";
import { createPortal } from "react-dom";

// 'modal-root' is a sibling to 'app-root'
const modalRoot = document.getElementById("modal-root");

function Modal({ isOpen, children }) {
  // element to which the modal will be rendered
  const el = document.createElement("div");

  useEffect(() => {
    // append to root when the children of Modal are mounted

    // do a cleanup
    return () => {
  }, [el]);

  return (
    isOpen &&
      // child element
          position: "absolute",
          top: 0,
          left: 0,
          height: "100%",
          width: "100%",
          padding: "100px",
          backgroundColor: "rgba(0,0,0,0.6)"
            width: "50%",
            background: "white",
            padding: "50px",
            textAlign: "center"
      // target container

export default function App() {
  const [isModalOpen, setModalOpen] = useState(false);

  const toggleModal = () => setModalOpen(!isModalOpen);

  return (
        position: "relative",
        overflow: "hidden"
      <button onClick={toggleModal}>open modal</button>

      <Modal isOpen={isModalOpen}>
        <button onClick={toggleModal}>close modal</button>

Top comments (2)

cathalmacdonnacha profile image
Cathal Mac Donnacha ๐Ÿš€ • Edited

Nice article but there are 2 issues:

  • You need to export the Modal component:

export default function Modal

  • You reference the id modal-root

const modalRoot = document.getElementById("modal-root");

but it should be just modal as this is what you set earlier in index.html

const modalRoot = document.getElementById("modal");

ottosamatori profile image

need ->
const globalDialog = useMemo(
() => document.getElementById(ROOT_DIALOG_ID) as HTMLElement,

const element = useMemo(() => document.createElement('section'), [open]);

prevent possible problems with the states of the other components contained

Top Posts from the React Ecosystem

1. Changes In The Official React Documentation

The former React Docs Beta has been officially released as the updated React documentation at after years of hard work and refinement. Check out the brand new React Docs: Whatโ€™s New in the Updated React Docs

2. CRA's Time is Over

React developer team has removed create-react-app (CRA) from official documentation rendering it no longer the default setup method for new projects. The bulky setup, slow, and outdated nature of CRA led to its removal: create-react-app is officially dead

3. How to Fetch Articles for Your Portfolio

Integrate the articles of your profile into your personal portfolio with either React, Vue, or Next.js by following these simple steps. It outlines how to include frontend to pull the information and correctly utilizes the API: How to Fetch Your Articles for Your Portfolio with React