Modals in React Part 1: What, Why, How

March 14th, 2021

This article assumes a basic familiarity with React, hooks and the concept of state.

This is a two part tutorial to create a reusable, accessible React modal component.

Part 1: This article, focuses on creating a Portal to house our Modal, and making it functional and reusable with props.

Part 2: Focuses on adding appropriate ARIA and controlling focus between the modal and the main page to make our component accessible (awesome).

It also assumes you probably read my previous article on Modals or are already familiar with the idea. If not, Read about Modals

What is a Portal?

Portals let us break out of our React application root div.

ReactDOM.render(
    <App />,
  document.getElementById('root')
)
JavaScript

Why use a Portal?

A React App contains components nested within each other, rendered into the root DOM element. Each nested component inherits aspects of its containing or parent element. For example, if our App has a width of 800px, and we render a Dashboard component in it, we can expect that our Dashboard will stay within the 800px constraint. In particular, if we add a z-index style or set overflow:hidden on a parent, our modal as a child element will not display correctly.

In order to get around the inheritance issue, we use portals, which allow us to render our modal in a div outside the root element.

Let's get started

I created a simple react app with create-react-app in the terminal.

npx create-react-app react-modal

In the public folder, open index.html and add a div with the id="modal" to contain our modal portal.

public/index.html
<body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!-- Add this -->
    <div id="modal"></div>  
    <!-- Add this -->
HTML

Modal Goals

  1. Reusable - fully customizable and can be used in other React projects

  2. Accessible - follows best practices for accessibility including tab focus, esc key response, and aria usage

This article is going to focus on the first point - creating a reusable component. In the next article, I will outline how to implement accessibility. I decided to split it because the article was becoming too long, and I wanted to spend a little extra time making sure I got the accessibility correct.

The modal will look like this:

Screen Shot 2021-03-15 at 11.48.36.png

It will have a title, description, and actions.

Setup:

Here is some starting code to make our modal meaningful. This takes a list of events and displays each in the DOM with a little button to delete. By the end of this article, when we click on the button to delete an event, a modal will appear, confirm we want to delete the event, and remove that card from the DOM.

In the src folder, create 3 files: events.js DisplayEvents.js EventCard.js

List of Events

src/events.js  // list of events to display

export const events = [
  {
    id: 1,
    title: 'Puppy Play Date',
    description: `A great opportunity to get our puppies together to run in the park`
  },
  {
    id: 2,
    title: 'Picnic in the Park',
    description: 'Let\'s all get together for a lovely picnic in the park'
  },
  {
    id: 3,
    title: 'Wine Tasting',
    description: "Sample this season's best local wines"
  },
  {
    id: 4,
    title: 'Pet Adoption',
    description: "Come out and adopt a cat or dog from the local shelter"
  },
  {
    id: 5,
    title: 'Rummage Sale',
    description: 'Neighborhood sale, hidden treasures await'
  },
  {
    id: 6,
    title: 'Language Lessons',
    description: "Learn the local dialect with friends"
  }
]

JavaScript

DisplayEvents - import the events from above and map them into the EventCard component (below). I also set up a useState hook and an updateEvents method to handle re-rendering the list when an event is deleted.

src/DisplayEvents.js

import React, { useState } from 'react'
import { events } from './events'
import EventCard from './EventCard'

function DisplayEvents() {
  const [eventList, setEventList] = useState(events)
  const updateEvents = (id) => {
    const newList = eventList.filter((event) => {
      return event.id !== id
    })
    setEventList(newList)
  }

  const renderEvents = () => {
    return eventList.map((event) => {
      return <EventCard event={event} key={event.id} updateEvents={updateEvents} />
    })
  }
  return <div>{renderEvents()}</div>
}

export default DisplayEvents
JavaScript

Here's the Card component, which simply takes the information from the list and displays the information for each event, and a delete button. We'e receiving the updateEvents method in the props, which we will use in our modal.

src/EventCard.js

import React from 'react'

const EventCard = ({ event, updateEvents }) => {
  return (
    <>
      <div className='card'>
        <div>
          <h4> {event.title} </h4>
          <p>{event.description}</p>
        </div>

        <button>Delete</button>
      </div>
    </>
  )
}

export default EventCard
JavaScript

Edit App.css and App.js

A little CSS to get us off the ground

src/App.css
.App {
  max-width: 600px;
  margin: 20px auto;
}

button {
  border-radius: 3px;
  padding: 7px;
  border: solid 1px #BBBFC3;
  cursor: pointer;
  margin: 0 7px;
}

.negative-button {
  background-color: #ff6961;
}

.card {
  max-width: 375px;
  box-shadow: 2px 2px 2px 1px rgba(208,213,217, .2);
  padding: 10px;
  margin: 5px auto;
  border: solid 1px #BBBFC3;
  border-radius: 3px;
}

/* Modal Styles */

.modal-bg {
  background-color: hsla(170, 5%, 15%, 0.8);
  position: fixed;
  top: 0;
  left:0;
  right: 0;
  bottom: 0;
}

.modal-window {
  width: 375px;
  height: 200px;
  border-radius: 5px;
  background-color: #fff;
  margin: 100px auto;
  padding: 20px;
}

@media screen and (max-width: 500px) {
  .modal-window {
    width: 250px;
    height: 250px;
  }
}

.modal-title {
  text-align: center;
}

.modal-actions {
  padding: 10px;
  text-align: center;
}
CSS

Edit our App to render DisplayEvents

src/App.js
import './App.css';
import DisplayEvents from './DisplayEvents'

function App() {
  return (
    <div className="App">
      <DisplayEvents/>        
    </div>
  );
}

export default App;
JavaScript

Okay, that's maybe a lot of starting code, but I wanted to make our modal meaningful. So, thanks for hanging in with me this far. Now, let's dive in!

Creating our Portal

Here we go! In our src folder, create a new file: Modal.js

Import React and ReactDom (provides specific methods to efficiently manage DOM elements, and where createPortal lives). We make a div for the dark overlay which will cover the page, and a div for the dialog window. We then inject it into the <div id="modal"> that we made in the public/index.html file.

import React from 'react'
import ReactDOM from 'react-dom'

const Modal = (props) => {
  return ReactDOM.createPortal(
    <div className="modal-bg">
      <div className='modal-window'>
      </div>
    </div>,
    document.querySelector('#modal')
  )
}

export default Modal
JavaScript

Great! Now how do we open it?

Let's go back to our EventCard Component. When we click the delete button we want the modal to appear.

Import { useState } from react, and create the open=false state. Toggling this value true will show the modal, false will hide it. In our return statement, add an onClick handler to our delete button which sets the state to true when clicked.

src/EventCard.js
import React, { useState } from 'react'

const EventCard = ({ event, updateEvents }) => {
  const [open, setOpen] = useState(false)
  return (
    <>
      <div className='card'>
        <div>
          <h4> {event.title} </h4>
          <p>{event.description}</p>
        </div>

        <button onClick={() => setOpen(true)}>
          Delete
        </button>
JavaScript

When open is true, display the modal by adding a conditional statement under the button.

// add import
import Modal from './Modal'

  ...
        <button onClick={() => setOpen(true)}>
          Delete
        </button>
        // add conditional
        {open && <Modal />}
  ...
JavaScript

Go ahead and try it out! It should look like this:

Screen Shot 2021-03-15 at 10.05.17.png

Next:

  • add some content with props

  • handle deleting an item

  • close that modal

Adding Content

We'll create space in our modal for a title, description and some actions to be displayed.

Modal.js
const Modal = (props) => {
  return ReactDOM.createPortal(
    <div className='modal-bg'>
      <div className='modal-window'>
        // add content
        <h3 className='modal-title'>{props.title}</h3>
        <p className='modal-description'>{props.description}</p>
        <div className='modal-actions'>{props.actions}</div>
      </div>
    </div>,
JavaScript

Back in our Event Card we'll create those props. The title and description are strings. The actions will be two buttons, one to confirm deleting the event and one to cancel.

EventCard.js
const EventCard = ({ event, updateEvents }) => {
  const [open, setOpen] = useState(false)

  // add content 
  const modalTitle = 'Delete Event'
  const modalDescription = `Are you sure you want to delete "${event.title}"?`
  const modalActions = () => {
    return (
      <>
        <button className='negative-button'>Delete</button>
        <button>Cancel</button>
      </>
    )
  }

  return (
    <>
      <div className='card'>
        <div>
          <h4> {event.title} </h4>
          <p>{event.description}</p>
        </div>

        <button onClick={() => setOpen(true)}>Delete</button>
        {open && (
          <Modal
          // add props
            title={modalTitle}
            description={modalDescription}
            actions={modalActions}
          />
        )}

JavaScript

Now the modal should look a little more interesting, but it's not really working yet.

Screen Shot 2021-03-15 at 10.29.45.png

Closing the Modal

Guess what we need? onClick event handlers for our modal action buttons! Let's get to it!

Clicking on the Delete button on the main page toggles the open state true and displays the modal. Toggling it false hides the modal. So, let's implement this on our modal cancel button.

EventCard.js
...
const modalActions = () => {
    return (
      <>
        <button className='negative-button'>Delete</button>
        <button
      // add onClick event handler
          onClick={() => {
            setOpen(false)
          }}
        >
          Cancel
        </button>
      </>
    )
  }
JavaScript

Did you get it opening and closing?

Now, for the modal delete button. It should delete the item and close the modal.

<button className='negative-button'
    // add click handler to close and delete
        onClick={()=> {
          updateEvents(event.id)
          setOpen(false)
        }}
        >Delete</button>
JavaScript

Alright! That's the basics I wanted to cover in this article:

Create a modal component which accepts props to customize its content and behavior.

This article has gotten a bit lengthy, so I want to take a break here.

Next time: Improving our Modal

  • improve accessibility with tab focus and arias

  • add a simple fade in - fade out animation to make transitions smooth

  • close the modal when the ESC key is pressed, or the overlay is clicked

Thanks for sticking with me to the end of this one! See you next time!

Here's the final code to this point.

DisplayEvents.js
import React, { useState } from 'react'
import { events } from './events'
import EventCard from './EventCard'

function DisplayEvents() {
  const [eventList, setEventList] = useState(events)
  const updateEvents = (id) => {
    const newList = eventList.filter((event) => {
      return event.id !== id
    })
    setEventList(newList)
  }

  const renderEvents = () => {
    return eventList.map((event) => {
      return <EventCard event={event} key={event.id} updateEvents={updateEvents} />
    })
  }
  return <div>{renderEvents()}</div>
}

export default DisplayEvents
JavaScript
EventCard.js
import React, { useState } from 'react'
import Modal from './Modal'

const EventCard = ({ event, updateEvents }) => {
  const [open, setOpen] = useState(false)
  const modalTitle = 'Delete Event'
  const modalDescription = `Are you sure you want to delete "${event.title}"?`

  const modalActions = () => {
    return (
      <>
        <button className='negative-button'
        onClick={()=> {
          updateEvents(event.id)
          setOpen(false)
        }}
        >Delete</button>
        <button
          onClick={() => {
            setOpen(false)
          }}
        >
          Cancel
        </button>
      </>
    )
  }

  return (
    <>
      <div className='card'>
        <div>
          <h4> {event.title} </h4>
          <p>{event.description}</p>
        </div>

        <button onClick={() => setOpen(true)}>Delete</button>
        {open && (
          <Modal
            title={modalTitle}
            description={modalDescription}
            actions={modalActions()}
          />
        )}
      </div>
    </>
  )
}

export default EventCard
JavaScript
Modal.js
import React from 'react'
import ReactDOM from 'react-dom'

const Modal = (props) => {
  return ReactDOM.createPortal(
    <div className='modal-bg'>
      <div className='modal-window'>
        <h3 className='modal-title'>{props.title}</h3>
        <p className='modal-description'>{props.description}</p>
        <div className='modal-actions'>{props.actions}</div>
      </div>
    </div>,
    document.querySelector('#modal')
  )
}

export default Modal
JavaScript

© 2025 Rebecca Page