Build a React Modal Component Example

A modal looks simple until you add one to a real internal dashboard. You need it to open from the correct employee row, close reliably, block accidental background clicks, and keep keyboard users from getting stuck.

I have used this pattern in support ticket tools, customer portals, and admin panels where users need a focused confirmation step. A reusable React modal component keeps that behavior consistent instead of rebuilding it on every page.

You will build a reusable modal for an employee directory, then add practical touches such as Escape-key support, overlay clicks, and accessible markup.

What Is a React Modal Component?

A React modal component is a focused UI layer that appears above the current page. It is useful when users must complete, review, or confirm an action without leaving their current screen.

For example, an HR admin may click “View Details” beside an employee. Instead of navigating to another route, the app can open a modal with that employee’s role, department, and contact details.

A modal is usually controlled with state. State is data that React remembers between renders. In this example, a parent component stores whether the modal is open and which employee is selected.

If you are new to React structure, it helps to understand how React props work before passing data into reusable components.

React Modal Component Example Setup

This tutorial uses a modern React 18+ application with functional components. A functional component is a JavaScript function that returns JSX, which is the HTML-like syntax React uses to describe the UI.

Create these files in your project:

src/
components/
Modal.jsx
Modal.css
EmployeeDirectory.jsx
App.jsx

The Modal component handles shared modal behavior. The employee directory decides when to show it and what employee data to display.

Create the Reusable React Modal Component

Start by creating src/components/Modal.jsx.

import { useEffect } from "react";
import "./Modal.css";

function Modal({ isOpen, title, children, onClose }) {
useEffect(() => {
function handleKeyDown(event) {
if (event.key === "Escape") {
onClose();
}
}

if (isOpen) {
window.addEventListener("keydown", handleKeyDown);
}

return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [isOpen, onClose]);

if (!isOpen) {
return null;
}

function handleOverlayClick(event) {
if (event.target === event.currentTarget) {
onClose();
}
}

return (
<div
className="modal-overlay"
role="presentation"
onMouseDown={handleOverlayClick}
>
<section
className="modal"
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
<div className="modal-header">
<h2 id="modal-title">{title}</h2>

<button
type="button"
className="modal-close-button"
onClick={onClose}
aria-label="Close modal"
>
×
</button>
</div>

<div className="modal-content">{children}</div>
</section>
</div>
);
}

export default Modal;

This component accepts four props:

  • isOpen decides whether React should display the modal.
  • title supplies the heading shown at the top.
  • children represents the content placed between the opening and closing Modal tags.
  • onClose is a function the parent provides to close the modal.

The if (!isOpen) line uses conditional rendering. It returns null when the modal is closed, so React renders nothing.

The useEffect hook listens for the Escape key only while the modal is open. A hook is a React function that lets a component use features such as state and browser side effects. The cleanup function removes the listener when the modal closes or unmounts, which prevents duplicate listeners.

For more examples of event-driven UI code, see handling events in React.

Add CSS for the Modal

Create src/components/Modal.css and add the following styles.

.modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: grid;
place-items: center;
padding: 24px;
background: rgba(15, 23, 42, 0.6);
}

.modal {
width: min(100%, 520px);
max-height: calc(100vh - 48px);
overflow-y: auto;
border-radius: 12px;
background: #ffffff;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.25);
}

.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 20px 24px;
border-bottom: 1px solid #e2e8f0;
}

.modal-header h2 {
margin: 0;
font-size: 1.25rem;
}

.modal-content {
padding: 24px;
}

.modal-close-button {
width: 36px;
height: 36px;
border: 0;
border-radius: 6px;
background: transparent;
color: #334155;
font-size: 1.75rem;
line-height: 1;
cursor: pointer;
}

.modal-close-button:hover {
background: #f1f5f9;
}

The overlay uses position: fixed, so it covers the full browser viewport even when the page scrolls. The z-index value places it above the rest of your React application.

The .modal element has a maximum height and vertical scrolling. This matters in real dashboards because content can grow when a modal includes forms, validation messages, or API-loaded details.

Pro Tip: In my experience, the most common modal bug is leaving document-level event listeners active after closing it. Always return a cleanup function from useEffect when you add key, resize, timer, or subscription logic.

Open the React Modal From a Parent Component

Now create src/EmployeeDirectory.jsx. This example shows a small employee directory in a browser-based admin application.

import { useState } from "react";
import Modal from "./components/Modal";

const employees = [
{
id: 101,
name: "Maya Patel",
role: "Support Manager",
department: "Customer Success",
email: "maya.patel@company.test",
},
{
id: 102,
name: "Daniel Kim",
role: "Frontend Developer",
department: "Engineering",
email: "daniel.kim@company.test",
},
{
id: 103,
name: "Olivia Brown",
role: "Sales Analyst",
department: "Revenue Operations",
email: "olivia.brown@company.test",
},
];

function EmployeeDirectory() {
const [selectedEmployee, setSelectedEmployee] = useState(null);

function openEmployeeModal(employee) {
setSelectedEmployee(employee);
}

function closeEmployeeModal() {
setSelectedEmployee(null);
}

return (
<main>
<h1>Employee Directory</h1>
<p>Select an employee to view account details.</p>

<div className="employee-list">
{employees.map((employee) => (
<article className="employee-card" key={employee.id}>
<h2>{employee.name}</h2>
<p>{employee.role}</p>

<button
type="button"
onClick={() => openEmployeeModal(employee)}
>
View details
</button>
</article>
))}
</div>

<Modal
isOpen={selectedEmployee !== null}
title={selectedEmployee?.name ?? "Employee details"}
onClose={closeEmployeeModal}
>
{selectedEmployee && (
<div>
<p>
<strong>Role:</strong> {selectedEmployee.role}
</p>
<p>
<strong>Department:</strong> {selectedEmployee.department}
</p>
<p>
<strong>Email:</strong> {selectedEmployee.email}
</p>

<button type="button" onClick={closeEmployeeModal}>
Close details
</button>
</div>
)}
</Modal>
</main>
);
}

export default EmployeeDirectory;

This component uses the useState hook to store selectedEmployee. Its initial value is null, which means no modal is open.

When a user clicks a “View details” button, the event handler passes that employee object into openEmployeeModal. React updates the state, the isOpen prop becomes true, and the reusable modal appears.

The optional chaining expression selectedEmployee?.name avoids an error while no employee has been selected. It safely reads name only when an employee object exists.

Notice the stable key={employee.id} value inside map(). React uses keys to track list items correctly when the data changes. Avoid using the array index for dynamic records.

Render the Employee Directory

Finally, update src/App.jsx.

import EmployeeDirectory from "./EmployeeDirectory";

function App() {
return <EmployeeDirectory />;
}

export default App;

This keeps App clean. The employee directory owns employee-specific state, while the modal stays generic enough to reuse for delete confirmations, edit forms, alerts, and preview panels.

If you are setting up a new project, this React environment setup tutorial can help you get the application running locally.

Improve the React Modal Component

The basic version works well for simple detail views. In larger React applications, I usually add a few improvements based on the modal’s job.

Add a Confirmation Action

A delete confirmation modal should accept an action callback. Keep the action in the parent component because the parent owns the data and API logic.

function DeleteEmployeeModal({ employee, isOpen, onCancel, onConfirm }) {
return (
<Modal
isOpen={isOpen}
title="Remove employee"
onClose={onCancel}
>
<p>
Remove {employee?.name} from the employee directory? This action cannot
be undone.
</p>

<div className="modal-actions">
<button type="button" onClick={onCancel}>
Cancel
</button>

<button
type="button"
className="danger-button"
onClick={() => onConfirm(employee.id)}
>
Remove employee
</button>
</div>
</Modal>
);
}

This component receives employee, onCancel, and onConfirm as props. It does not make the deletion itself. That separation makes the modal easier to test and reuse in other screens.

If deletion requires a backend call, show loading and error handling states in the parent. Disable the confirmation button during the API request so users cannot submit the same action twice.

Restore Focus After Closing

A polished modal should return focus to the button that opened it. This is especially important for keyboard users moving through an admin panel.

import { useRef, useState } from "react";
import Modal from "./components/Modal";

function EmployeeCard({ employee }) {
const [isOpen, setIsOpen] = useState(false);
const openButtonRef = useRef(null);

function closeModal() {
setIsOpen(false);
openButtonRef.current?.focus();
}

return (
<>
<button
ref={openButtonRef}
type="button"
onClick={() => setIsOpen(true)}
>
View {employee.name}
</button>

<Modal
isOpen={isOpen}
title={employee.name}
onClose={closeModal}
>
<p>{employee.role}</p>
</Modal>
</>
);
}

export default EmployeeCard;

You can see the output in the screenshot below.

Build React Modal Component Example

The useRef hook stores a reference to the actual browser button without causing another render. After the modal closes, focus() puts keyboard focus back where the user started.

For a simple modal, this is enough. For a complex production dialog, also trap Tab key focus inside the modal so users cannot tab into the page behind it.

Things to Keep in Mind

  • Keep state in the parent: Store whether the modal is open and its selected data in the closest shared parent component.
  • Clean up side effects: Remove Escape-key listeners, timers, and subscriptions in the useEffect cleanup function.
  • Use accessible labels: Add role="dialog", aria-modal="true", a useful title, and a clear close-button label.
  • Prevent duplicate submissions: Disable destructive or save buttons while an API request is running.
  • Avoid hiding errors: Show an actionable message if a save or delete request fails inside the modal.
  • Do not expose secrets: Never place private API keys, passwords, or server credentials in frontend React code.

Frequently Asked Questions

How do I open a modal in React?

Store an open value in state with useState. Set it to true in a button click event handler, then pass that value to the modal through an isOpen prop.

How do I close a React modal when clicking outside it?

Attach an event handler to the overlay element. Compare event.target with event.currentTarget, then close only when users click the overlay itself rather than the modal content.

Why does my React modal not close with Escape?

Make sure the keyboard listener is added when the modal opens. Also return a useEffect cleanup function so old listeners do not remain active after repeated opens and closes.

Should a modal manage its own open state?

Usually, no. The parent should manage open state because it knows which record, form, or action the modal represents. The modal should receive isOpen and onClose through props.

Do I need a library to build a React modal?

No. You can build a useful modal with React state, event handlers, JSX, and CSS. A library can help later when you need advanced focus management, animations, nested dialogs, or a full design system.

Can I load API data inside a React modal?

Yes. Use useEffect when the modal opens and show loading, success, and error states. Cancel or ignore outdated requests if users can switch records before a response arrives.

A reusable React modal component starts with a small contract: open state, a close callback, a title, and flexible content. Test opening, closing, keyboard behavior, and real API states one piece at a time, then extend the component only when your application needs more. I hope you found this article helpful.

You May Also Like

Leave a Comment

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.