@reach/dialog and react-modal are both React libraries designed to help developers create accessible modal dialogs that comply with ARIA standards. They handle critical accessibility concerns such as focus trapping, screen reader announcements, and preventing background interaction while the modal is open. @reach/dialog was part of the Reach UI suite focused on accessibility-first primitives, while react-modal is a long-standing, widely used modal implementation that provides similar functionality with a different API design and customization approach.
Both @reach/dialog and react-modal solve the same core problem — creating accessible modal dialogs in React — but they take very different approaches to API design, styling, and developer experience. Let’s compare how they handle real-world requirements.
@reach/dialog is deprecated. As of 2021, the Reach UI project has been archived, and its maintainers recommend migrating to Radix UI. While the package still works, it receives no updates or security patches. Do not use it in new projects.
react-modal remains actively maintained with regular releases and issue triage. It’s safe for production use in both new and existing applications.
💡 Recommendation: For new projects, consider Radix UI Dialog instead of
@reach/dialog. This comparison assumes you’re evaluating these two for legacy compatibility or migration planning.
@reach/dialog ships with zero CSS. You style everything yourself using standard CSS or CSS-in-JS. The modal and overlay are plain <div> elements with no default appearance.
// @reach/dialog: Fully unstyled
import { DialogOverlay, DialogContent } from "@reach/dialog";
import "@reach/dialog/styles.css"; // Optional reset-only styles
function MyModal({ isOpen, onClose }) {
return (
<DialogOverlay isOpen={isOpen} onDismiss={onClose}>
<DialogContent>
<h2>My Modal</h2>
<button onClick={onClose}>Close</button>
</DialogContent>
</DialogOverlay>
);
}
/* You must define all styles */
.DialogOverlay {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
background: rgba(0, 0, 0, 0.5);
}
.DialogContent {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 2rem;
}
react-modal provides default styling that you override via props or CSS classes. It expects you to define at least an overlay style, but includes basic positioning.
// react-modal: Styled via props or classes
import Modal from "react-modal";
// Set app root for accessibility
Modal.setAppElement('#root');
function MyModal({ isOpen, onClose }) {
return (
<Modal
isOpen={isOpen}
onRequestClose={onClose}
style={{
overlay: {
backgroundColor: 'rgba(0, 0, 0, 0.75)'
},
content: {
top: '50%',
left: '50%',
right: 'auto',
bottom: 'auto',
marginRight: '-50%',
transform: 'translate(-50%, -50%)'
}
}}
>
<h2>My Modal</h2>
<button onClick={onClose}>Close</button>
</Modal>
);
}
@reach/dialog handles all ARIA attributes automatically. It injects role="dialog", manages focus trapping, and ensures screen readers announce the dialog properly without any extra work.
// @reach/dialog: Zero-config accessibility
<DialogOverlay>
<DialogContent aria-labelledby="modal-title">
<h1 id="modal-title">Settings</h1>
{/* Focus automatically trapped inside */}
</DialogContent>
</DialogOverlay>
// Automatically gets:
// - role="dialog"
// - aria-modal="true"
// - Proper focus management
react-modal requires explicit setup for full accessibility compliance. You must:
Modal.setAppElement() to hide background content from screen readersaria labels manually// react-modal: Manual accessibility setup
Modal.setAppElement('#root'); // Hides main app from screen readers
<Modal
isOpen={isOpen}
onRequestClose={onClose}
contentLabel="Settings Modal" // Required for accessibility
aria={{ labelledby: "modal-title" }}
>
<h1 id="modal-title">Settings</h1>
{/* Focus trapping works by default */}
</Modal>
@reach/dialog uses a two-component pattern: DialogOverlay (handles backdrop and portal) and DialogContent (the actual modal). This makes composition and animation easier.
// @reach/dialog: Composable structure
<DialogOverlay>
<CustomBackdrop />
<DialogContent>
<ModalHeader />
<ModalBody />
<ModalFooter />
</DialogContent>
</DialogOverlay>
react-modal is a single component that wraps your content. Customizing the overlay requires overriding styles or using the overlayClassName prop.
// react-modal: Single wrapper
<Modal>
<div className="modal-container">
<ModalHeader />
<ModalBody />
<ModalFooter />
</div>
</Modal>
@reach/dialog uses a declarative approach. Open/close state is controlled via the isOpen prop, and dismissal is handled through onDismiss.
// @reach/dialog: Declarative control
<DialogOverlay
isOpen={isModalOpen}
onDismiss={() => setIsModalOpen(false)}
>
{/* ... */}
</DialogOverlay>
react-modal relies on callbacks like onRequestClose (triggered by ESC key, overlay click, or close button) and onAfterOpen for post-open logic.
// react-modal: Callback-driven
<Modal
isOpen={isModalOpen}
onRequestClose={() => setIsModalOpen(false)}
onAfterOpen={() => {
// Focus first input, etc.
}}
>
{/* ... */}
</Modal>
@reach/dialog Shines (Legacy Context)react-modal WinsSince @reach/dialog is deprecated, here’s how you’d migrate a basic example to Radix UI Dialog (the official successor):
// Radix UI Dialog (modern replacement)
import * as Dialog from '@radix-ui/react-dialog';
function MyModal({ isOpen, onClose }) {
return (
<Dialog.Root open={isOpen} onOpenChange={onClose}>
<Dialog.Portal>
<Dialog.Overlay className="DialogOverlay" />
<Dialog.Content className="DialogContent">
<Dialog.Title>Settings</Dialog.Title>
<button onClick={onClose}>Close</button>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
| Feature | @reach/dialog | react-modal |
|---|---|---|
| Status | ❌ Deprecated | ✅ Actively maintained |
| Styling | 🎨 Zero CSS, fully unstyled | 🖌️ Default styles, override via props |
| Accessibility | 🤖 Fully automatic | 🛠️ Requires manual setup |
| Component Model | 🧩 Composable (Overlay + Content) | 📦 Single wrapper component |
| API Style | 📜 Declarative (isOpen prop) | 📞 Callback-driven (onRequestClose) |
| Learning Curve | 📉 Low (if you understand ARIA) | 📈 Medium (more config options) |
@reach/dialog with active development and better TypeScript support.@reach/dialog apps: Plan a migration to Radix UI when feasible.react-modal users: Continue using it confidently — it’s stable, well-documented, and handles most modal use cases effectively.Remember: The best modal library is the one that keeps your dialogs accessible, maintainable, and aligned with your team’s workflow — but never choose a deprecated package for new work.
Choose @reach/dialog if you prioritize strict adherence to WAI-ARIA authoring practices and want a minimal, unstyled component that gives you full control over styling while handling all accessibility mechanics correctly out of the box. It’s ideal for design systems or applications where you need pixel-perfect UI control without fighting default styles. Note that Reach UI has been deprecated in favor of Radix UI, so new projects should consider alternatives unless maintaining an existing codebase.
Choose react-modal if you need a battle-tested, flexible modal solution with built-in support for overlay styling, portal rendering, and lifecycle callbacks. It’s well-suited for applications that require deep customization of both modal and overlay appearance, and where you’re comfortable managing some accessibility details manually (like labeling). The package remains actively maintained and stable for production use.
An accessible dialog or modal window.
import { Dialog } from "@reach/dialog";
import "@reach/dialog/styles.css";
function Example(props) {
const [showDialog, setShowDialog] = React.useState(false);
const open = () => setShowDialog(true);
const close = () => setShowDialog(false);
return (
<div>
<button onClick={open}>Open Dialog</button>
<Dialog isOpen={showDialog} onDismiss={close}>
<button className="close-button" onClick={close}>
<VisuallyHidden>Close</VisuallyHidden>
<span aria-hidden>×</span>
</button>
<p>Hello there. I am a dialog</p>
</Dialog>
</div>
);
}