react-aria vs react-focus-lock vs react-focus-on vs react-modal
Accessible Modal and Focus Management in React
react-ariareact-focus-lockreact-focus-onreact-modalSimilar Packages:

Accessible Modal and Focus Management in React

These libraries address the complex challenges of building modal dialogs and managing keyboard focus in React applications. react-modal provides a ready-to-use modal component with built-in styles, while react-focus-lock and react-focus-on offer lower-level primitives for trapping focus within custom UIs. react-aria (Adobe) delivers a comprehensive set of unstyled hooks for building fully accessible components from scratch, adhering to WAI-ARIA standards. Choosing between them depends on whether you need a quick drop-in solution, a focus utility for custom designs, or a complete accessibility foundation for a design system.

Npm Package Weekly Downloads Trend

3 Years

Github Stars Ranking

Stat Detail

Package
Downloads
Stars
Size
Issues
Publish
License
react-aria015,60014.7 MB57311 days agoApache-2.0
react-focus-lock01,388111 kB67 months agoMIT
react-focus-on035059.2 kB06 months agoMIT
react-modal07,408188 kB2112 years agoMIT

Accessible Modal and Focus Management: react-aria vs react-focus-lock vs react-focus-on vs react-modal

Building modal dialogs in React involves more than just showing a div on top of content. You must trap keyboard focus, manage screen reader announcements, prevent background scrolling, and ensure the dialog is dismissible. The four packages listed here solve these problems at different levels of abstraction. Let's examine how they handle the core requirements of a modal implementation.

🧱 Component Structure: Ready-Made vs Headless Hooks

react-modal provides a complete <Modal> component. You pass content as children and control visibility with a boolean prop. It renders a portal automatically.

// react-modal: Complete component
import Modal from 'react-modal';

function MyModal({ isOpen, onClose }) {
  return (
    <Modal isOpen={isOpen} onRequestClose={onClose}>
      <h2>Dialog Title</h2>
      <p>Content goes here.</p>
    </Modal>
  );
}

react-aria gives you hooks like useModal and useDialog. You build the markup yourself, which allows full styling control but requires more setup.

// react-aria: Headless hooks
import { useModal, useDialog, FocusScope } from '@react-aria';

function MyModal({ isOpen, onClose }) {
  const ref = useRef(null);
  const { modalProps } = useModal();
  const { dialogProps, titleProps } = useDialog({}, ref);

  if (!isOpen) return null;

  return (
    <FocusScope restoreFocus>
      <div {...modalProps} ref={ref} {...dialogProps}>
        <h2 {...titleProps}>Dialog Title</h2>
        <p>Content goes here.</p>
        <button onClick={onClose}>Close</button>
      </div>
    </FocusScope>
  );
}

react-focus-lock is a wrapper component. You place it around your custom modal content to enforce focus trapping.

// react-focus-lock: Focus wrapper
import FocusLock from 'react-focus-lock';

function MyModal({ isOpen, onClose }) {
  if (!isOpen) return null;

  return (
    <FocusLock>
      <div className="modal">
        <h2>Dialog Title</h2>
        <button onClick={onClose}>Close</button>
      </div>
    </FocusLock>
  );
}

react-focus-on works similarly to react-focus-lock but is designed to be the main container for your modal, handling more side effects automatically.

// react-focus-on: Enhanced wrapper
import { FocusOn } from 'react-focus-on';

function MyModal({ isOpen, onClose }) {
  return (
    <FocusOn enabled={isOpen}>
      <div className="modal">
        <h2>Dialog Title</h2>
        <button onClick={onClose}>Close</button>
      </div>
    </FocusOn>
  );
}

🔒 Focus Trapping Mechanisms

Focus trapping prevents users from tabbing outside the modal. This is critical for accessibility.

react-modal handles this internally. You do not need to configure it, but you cannot easily change the logic.

// react-modal: Internal handling
// No extra code needed for focus trap
<Modal isOpen={true}>
  <button>First Focusable</button>
  <button>Last Focusable</button>
</Modal>

react-aria uses FocusScope to manage focus restoration and trapping. You must wrap your content explicitly.

// react-aria: Explicit FocusScope
import { FocusScope } from '@react-aria';

<FocusScope restoreFocus contain>
  <button>First Focusable</button>
  <button>Last Focusable</button>
</FocusScope>

react-focus-lock uses the FocusLock component to constrain focus. It returns focus to the trigger element when unmounted.

// react-focus-lock: Constrain focus
import FocusLock from 'react-focus-lock';

<FocusLock returnFocus>
  <button>First Focusable</button>
  <button>Last Focusable</button>
</FocusLock>

react-focus-on enables focus locking via the enabled prop. It is robust against dynamic content changes.

// react-focus-on: Enabled prop
import { FocusOn } from 'react-focus-on';

<FocusOn enabled={true}>
  <button>First Focusable</button>
  <button>Last Focusable</button>
</FocusOn>

🙈 Hiding Background Content (aria-hidden)

When a modal opens, screen readers should ignore the background content. This is often overlooked.

react-modal sets aria-hidden on the app root automatically if you configure appElement.

// react-modal: Configure app element
Modal.setAppElement('#root');

<Modal isOpen={true}>...</Modal>
// Background #root gets aria-hidden="true" automatically

react-aria requires you to manage aria-hidden manually or use useOverlay which helps coordinate this state.

// react-aria: Manual or useOverlay
// You typically manage state to hide siblings
<div aria-hidden={isModalOpen}>Background Content</div>

react-focus-lock does not handle aria-hidden by default. You must implement this logic yourself.

// react-focus-lock: Manual implementation
// No built-in API for aria-hidden on siblings
<div className={isOpen ? 'hidden' : ''}>Background</div>

react-focus-on automatically sets aria-hidden on elements outside the lock when enabled. This is a key advantage over react-focus-lock.

// react-focus-on: Automatic aria-hidden
<FocusOn enabled={true}>
  {/* Outside content is automatically hidden from AT */}
</FocusOn>

📜 Scroll Locking

Preventing the background page from scrolling while the modal is open is a standard UX requirement.

react-modal provides shouldCloseOnEsc and scroll prevention via CSS on the body, often requiring custom styles.

// react-modal: CSS based
// You often need to add a class to body manually or via onRequestOpen
<Modal onRequestOpen={() => document.body.classList.add('no-scroll')}>
  ...
</Modal>

react-aria does not lock scroll by default. You need to use usePreventScroll hook.

// react-aria: usePreventScroll hook
import { usePreventScroll } from '@react-aria';

function Modal() {
  usePreventScroll(); // Locks body scroll
  return <div>...</div>;
}

react-focus-lock does not lock scroll. It focuses only on focus management.

// react-focus-lock: No scroll lock
// Requires external solution like react-remove-scroll
<FocusLock>
  <div>Content</div>
</FocusLock>

react-focus-on includes scroll locking out of the box. It combines react-focus-lock and react-remove-scroll internally.

// react-focus-on: Built-in scroll lock
<FocusOn enabled={true}>
  <div>Content</div>
  {/* Body scroll is locked automatically */}
</FocusOn>

🎨 Styling and Customization

How much control do you have over the look and feel?

react-modal comes with default styles that look dated. You must pass className and overlayClassName to override them completely.

// react-modal: Override styles
<Modal
  className="my-custom-modal"
  overlayClassName="my-custom-overlay"
>
  Content
</Modal>

react-aria is unstyled. You apply all classes and styles. This is best for design systems.

// react-aria: Fully custom
<div {...props} className="flex items-center justify-center">
  <div className="bg-white p-4 rounded">Content</div>
</div>

react-focus-lock is unstyled. It renders a div by default but accepts custom wrappers.

// react-focus-lock: Unstyled
<FocusLock className="my-focus-wrapper">
  <div>Content</div>
</FocusLock>

react-focus-on is unstyled. It focuses on behavior, leaving visuals to you.

// react-focus-on: Unstyled
<FocusOn>
  <div className="modal-styles">Content</div>
</FocusOn>

🛠️ Maintenance and Ecosystem

react-modal is a legacy library. It is stable but rarely updated with new React patterns. It is not recommended for new greenfield projects.

react-aria is actively maintained by Adobe. It is part of a larger ecosystem (react-spectrum) and follows modern React standards.

react-focus-lock and react-focus-on are maintained by the same author. They are widely used in the community and updated regularly to support React concurrent features.

📊 Summary: Key Differences

Featurereact-modalreact-ariareact-focus-lockreact-focus-on
TypeFull ComponentHooks SuiteFocus PrimitiveEnhanced Primitive
StylingDefault + OverrideUnstyledUnstyledUnstyled
Focus TrapBuilt-inFocusScopeFocusLockFocusOn
Scroll LockManual CSSusePreventScrollNo (External)Built-in
aria-hiddenAutomatic (Root)Manual / HookNoBuilt-in
Best ForLegacy / QuickDesign SystemsCustom FocusCustom Modals

💡 The Big Picture

react-modal is the old guard. It works, but it fights against modern styling systems like Tailwind or CSS-in-JS. Use it only if you need a modal tomorrow and don't care about perfect design integration.

react-aria is the professional choice for building systems. It requires more code upfront but guarantees accessibility compliance and flexibility. It is the foundation for custom design systems.

react-focus-lock is a utility belt item. Use it when you have a modal already but realize you forgot to trap focus. It fixes one specific problem well.

react-focus-on is the sweet spot for custom modals. It handles the annoying side effects (scroll, aria-hidden, focus) so you can focus on the UI. It is often the best choice for single-off custom modals in an app.

How to Choose: react-aria vs react-focus-lock vs react-focus-on vs react-modal

  • react-aria:

    Choose react-aria if you are building a design system or need full control over markup and styling while ensuring strict WAI-ARIA compliance. It is ideal for teams that want to implement custom interactive patterns without reinventing accessibility logic, as it provides low-level hooks like useModal and useFocusTrap that integrate with your own components.

  • react-focus-lock:

    Choose react-focus-lock if you already have a modal or drawer component and simply need to ensure keyboard focus stays trapped inside it. It is a lightweight, surgical tool for focus management that does not impose styles or handle aria-hidden attributes, making it perfect for adding accessibility to existing custom UIs.

  • react-focus-on:

    Choose react-focus-on if you want a slightly higher-level abstraction than react-focus-lock that also handles aria-hidden on outside elements and body scroll locking. It is suitable for custom modals where you need focus trapping plus basic accessibility and UX enhancements without the overhead of a full component library.

  • react-modal:

    Choose react-modal only for legacy projects or internal tools where speed is more critical than design consistency. It is a complete solution with default styles, but it is less flexible for modern, headless architectures and may require significant effort to override default CSS for a custom look.

README for react-aria

React Aria

A library of React Hooks that provides accessible UI primitives for your design system.

Features

  • ♿️ Accessible – React Aria provides accessibility and behavior according to WAI-ARIA Authoring Practices, including full screen reader and keyboard navigation support. All components have been tested across a wide variety of screen readers and devices to ensure the best experience possible for all users.
  • 📱 Adaptive – React Aria ensures consistent behavior, no matter the UI. It supports mouse, touch, keyboard, and screen reader interactions that have been tested across a wide variety of browsers, devices, and platforms.
  • 🌍 International – React Aria supports over 30 languages, including right-to-left-specific behavior, internationalized date and number formatting, and more.
  • 🎨 Fully customizable – React Aria doesn’t implement any rendering or impose a DOM structure, styling methodology, or design-specific details. It provides behavior, accessibility, and interactions and lets you focus on your design.

Getting started

The easiest way to start building a component library with React Aria is by following our getting started guide. It walks through all of the steps needed to install the hooks from npm, and create your first component.

Example

Here is a very basic example of using React Aria.

import {useButton} from '@react-aria/button';

function Button(props) {
  let ref = React.useRef();
  let {buttonProps} = useButton(props, ref);

  return (
    <button {...buttonProps} ref={ref}>
      {props.children}
    </button>
  );
}

<Button onPress={() => alert('Button pressed!')}>Press me</Button>

Learn more

React Aria is part of a family of libraries that help you build adaptive, accessible, and robust user experiences. Learn more about React Spectrum and React Stately on our website.