react-modal vs @reach/dialog
Accessible Modal Dialogs in React Applications
react-modal@reach/dialogSimilar Packages:
Accessible Modal Dialogs in React Applications

@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.

Npm Package Weekly Downloads Trend
3 Years
Github Stars Ranking
Stat Detail
Package
Downloads
Stars
Size
Issues
Publish
License
react-modal2,169,8257,410188 kB203a year agoMIT
@reach/dialog254,9176,00353.3 kB100-MIT

Accessible Modals Compared: @reach/dialog vs react-modal

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.

⚠️ Deprecation Status: A Critical Starting Point

@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.

🎨 Styling Approach: Unstyled Primitives vs Opinionated Defaults

@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>
  );
}

🔒 Accessibility Mechanics: Automatic vs Manual Setup

@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:

  1. Call Modal.setAppElement() to hide background content from screen readers
  2. Provide aria labels manually
  3. Manage focus behavior if needed
// 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>

🧩 Component Structure: Composable vs Monolithic

@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>

🔄 Lifecycle and Events: Declarative vs Callback-Driven

@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>

📏 Real-World Tradeoffs

When @reach/dialog Shines (Legacy Context)

  • You have a strict design system requiring zero opinionated styles
  • Your team deeply understands accessibility and wants minimal abstraction
  • You’re already using other Reach UI components

When react-modal Wins

  • You need a quick, working modal with reasonable defaults
  • Your app requires complex overlay interactions (e.g., draggable modals)
  • You prefer configuring behavior through props rather than composing components
  • You’re building a new application and want active maintenance

🛑 Critical Migration Note

Since @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>
  );
}

🆚 Summary: Key Differences

Feature@reach/dialogreact-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)

💡 Final Guidance

  • For new projects: Use Radix UI Dialog — it’s the spiritual successor to @reach/dialog with active development and better TypeScript support.
  • For existing @reach/dialog apps: Plan a migration to Radix UI when feasible.
  • For 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.

How to Choose: react-modal vs @reach/dialog
  • react-modal:

    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.

  • @reach/dialog:

    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.

README for react-modal

react-modal

Accessible modal dialog component for React.JS

Build Status Coverage Status gzip size Join the chat at https://gitter.im/react-modal/Lobby

Table of Contents

Installation

To install, you can use npm or yarn:

$ npm install --save react-modal
$ yarn add react-modal

To install react-modal in React CDN app:

  • Add this CDN script tag after React CDN scripts and before your JS files (for example from cdnjs):

       <script src="https://cdnjs.cloudflare.com/ajax/libs/react-modal/3.14.3/react-modal.min.js"
       integrity="sha512-MY2jfK3DBnVzdS2V8MXo5lRtr0mNRroUI9hoLVv2/yL3vrJTam3VzASuKQ96fLEpyYIT4a8o7YgtUs5lPjiLVQ=="
       crossorigin="anonymous"
       referrerpolicy="no-referrer"></script>
    
  • Use <ReactModal> tag inside your React CDN app.

API documentation

The primary documentation for react-modal is the reference book, which describes the API and gives examples of its usage.

Examples

Here is a simple example of react-modal being used in an app with some custom styles and focusable input elements within the modal content:

import React from 'react';
import ReactDOM from 'react-dom';
import Modal from 'react-modal';

const customStyles = {
  content: {
    top: '50%',
    left: '50%',
    right: 'auto',
    bottom: 'auto',
    marginRight: '-50%',
    transform: 'translate(-50%, -50%)',
  },
};

// Make sure to bind modal to your appElement (https://reactcommunity.org/react-modal/accessibility/)
Modal.setAppElement('#yourAppElement');

function App() {
  let subtitle;
  const [modalIsOpen, setIsOpen] = React.useState(false);

  function openModal() {
    setIsOpen(true);
  }

  function afterOpenModal() {
    // references are now sync'd and can be accessed.
    subtitle.style.color = '#f00';
  }

  function closeModal() {
    setIsOpen(false);
  }

  return (
    <div>
      <button onClick={openModal}>Open Modal</button>
      <Modal
        isOpen={modalIsOpen}
        onAfterOpen={afterOpenModal}
        onRequestClose={closeModal}
        style={customStyles}
        contentLabel="Example Modal"
      >
        <h2 ref={(_subtitle) => (subtitle = _subtitle)}>Hello</h2>
        <button onClick={closeModal}>close</button>
        <div>I am a modal</div>
        <form>
          <input />
          <button>tab navigation</button>
          <button>stays</button>
          <button>inside</button>
          <button>the modal</button>
        </form>
      </Modal>
    </div>
  );
}

ReactDOM.render(<App />, appElement);

You can find more examples in the examples directory, which you can run in a local development server using npm start or yarn run start.

Demos

There are several demos hosted on CodePen which demonstrate various features of react-modal: