Modal Dialog Portal
Modal Dialog Portal
Modal dialogs are the most common use case for Portals. A modal needs to cover the entire
viewport, appear above all other content, and block interaction with the rest of the page.
index.html
<div id="root"></div>
<div id="modal-root"></div>
Modal.jsx – Reusable Modal Component
import { createPortal } from 'react-dom';
import { useEffect, useRef } from 'react';
function Modal({ isOpen, onClose, title, children }) {
const overlayRef = useRef(null);
// Close when clicking the backdrop (outside the dialog)
function handleOverlayClick(e) {
if (e.target === overlayRef.current) {
onClose();
}
}
// Close on Escape key press
useEffect(() => {
if (!isOpen) return;
function handleKeyDown(e) {
if (e.key === 'Escape') onClose();
}
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, [isOpen, onClose]);
// Prevent background scroll while modal is open
useEffect(() => {
if (isOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = '';
}
return () => { document.body.style.overflow = ''; };
}, [isOpen]);
if (!isOpen) return null;
return createPortal(
<div
ref={overlayRef}
onClick={handleOverlayClick}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
style={{
position: 'fixed',
inset: 0,
backgroundColor: 'rgba(0,0,0,0.5)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999
}}
>
<div style={{ background: '#fff', padding: '2rem', borderRadius: '8px', minWidth: '320px' }}>
<h2 id="modal-title">{title}</h2>
{children}
<button onClick={onClose}>Close</button>
</div>
</div>,
document.getElementById('modal-root')
);
}
export default Modal;
App.jsx: Using the Modal
import { useState } from 'react';
import Modal from './Modal';
function App() {
const [open, setOpen] = useState(false);
return (
<div>
<h1>My Application</h1>
<button onClick={() => setOpen(true)}>Open Modal</button>
<Modal
isOpen={open}
onClose={() => setOpen(false)}
title="Confirm Action"
>
<p>Are you sure you want to proceed?</p>
<button onClick={() => setOpen(false)}>Yes, proceed</button>
</Modal>
</div>
);
}
export default App;
Key Concepts in the Modal Example
- Conditional rendering: Return
nullwhenisOpenis false, nothing is mounted in the portal target. - Backdrop click: Compare
e.targetwith the overlay ref to close only when clicking outside the dialog box. - Escape key: Add a
keydownlistener inside auseEffect, clean it up on unmount. - Body overflow lock: Prevent page scroll while the modal is visible.
- ARIA attributes:
role="dialog",aria-modal="true",aria-labelledbyfor accessibility.
