· By John Kavanagh

Building Accessible Modals, Drawers, and Popovers in React

Abstract image used to represent Accessible Modals, Drawers, and Popovers in React
Image by Scott Evans.

In Brief

Start by choosing the right pattern: a modal dialog, drawer and popover do not share the same interaction contract. Give the overlay appropriate semantics and an accessible name, move focus deliberately, support Escape where expected, restore focus on close, and stop background content remaining interactive for modal experiences. Test keyboard, screen‑reader, pointer, scroll and stacked‑overlay behaviour, not just the opening animation.

Overlays are one of those UI patterns that can look finished long before they are actually finished.

The modal opens. The drawer slides in. The popover appears beside the button. On a happy‑path mouse demo, everything seems fine. Then someone uses a keyboard, a screen reader, zoom, or a smaller viewport, and the rough edges appear quickly: focus disappears, Escape stops working, background content stays interactive, scroll position jumps, or the overlay announces itself with the wrong semantics.

That is why overlay work deserves more care than the animation alone.


Start by Choosing the Right Overlay

Teams sometimes treat modal, drawer, and popover as purely visual variants. They are not.

  • a modal dialog interrupts the current task and usually traps focus
  • a drawer is often still a dialog, just presented from an edge
  • a popover is usually lighter weight and often should not behave like a full modal

If we choose the wrong interaction pattern first, the accessibility behaviour becomes muddled very quickly.

The visual design should follow the interaction contract, not the other way around.


A Modal Dialog Has a Minimum Accessibility Contract

If we are building a genuine modal dialog, the minimum bar is usually:

  • move focus into the dialog when it opens
  • keep keyboard focus inside whilst it remains open
  • return focus to the triggering control when it closes
  • provide an accessible name
  • let users dismiss it predictably
  • prevent background content from behaving as though it is still active

For a small modal, I'd start with the native dialog element. Calling showModal() puts it in the browser's top layer and makes the rest of the document inert. This TypeScript example keeps opening and closing under React's control: the parent passes isOpen and sets it to false when onClose runs.

import { useEffect, useId, useRef } from 'react';
import type { ReactElement, ReactNode } from 'react';

type DialogProps = {
  children: ReactNode;
  isOpen: boolean;
  onClose: () => void;
  title: string;
};

export const Dialog = ({
  children,
  isOpen,
  onClose,
  title
}: DialogProps): ReactElement => {
  const dialogRef = useRef<HTMLDialogElement | null>(null);
  const titleId = useId();

  useEffect(() => {
    const dialog = dialogRef.current;
    if (!isOpen || !dialog) return;

    const previous = document.activeElement;
    dialog.showModal();

    return () => {
      dialog.close();
      if (previous instanceof HTMLElement && previous.isConnected) {
        previous.focus();
      }
    };
  }, [isOpen]);

  return (
    <dialog
      aria-labelledby={titleId}
      onCancel={(event) => {
        event.preventDefault();
        event.stopPropagation();
        onClose();
      }}
      ref={dialogRef}
    >
      <h2 id={titleId}>{title}</h2>
      <button onClick={onClose} type="button">Close</button>
      {children}
    </dialog>
  );
};

The browser moves focus into the dialog and prevents interaction with the background. The Close button and Escape both ask the parent to close it; clicking the backdrop deliberately does nothing. The effect closes the dialog and restores focus to the previous control if it still exists. Server rendering produces a closed dialog, and showModal() runs after mounting. In the Next.js App Router, put this component behind a client boundary. A native modal needs no portal for stacking; a custom portal targeting document.body must wait until that browser node exists.

This covers a small, controlled dialog, not every overlay requirement. Choose a suitable initial focus target for longer content, and check scroll locking, mobile viewports and zoom in your application. Native modal stacking makes underlying dialogs inert, but nested flows still need testing: close the top dialog first and check where focus returns.


Focus Management is Where Many Home‑Grown Overlays Fail

Opening the overlay is only half the job. Focus has to land somewhere useful inside it.

That often means:

  • the dialog container if there is no better first target
  • the first interactive control
  • or a deliberately chosen element such as the heading or primary action

On close, focus should usually return to the control that opened the overlay. That part matters more than people expect because it preserves orientation for keyboard and assistive technology users.

If the user closes a modal and focus vanishes to the body, the interaction feels broken even when the overlay itself looked polished.


Drawers Usually Inherit the Same Semantics as Modals

A drawer presented from the side often behaves as a modal dialog. The animation direction does not change the accessibility requirement.

If the drawer blocks the main task until dismissed, it still needs dialog semantics, focus management, dismissal behaviour, and background inertness.

The common mistake is to build the drawer as a visual panel only, without the dialog behaviour the experience actually implies.


Popovers are Different Because They are Often Non‑Modal

This is where teams over‑apply modal rules.

A popover tied to a button, disclosure, or formatting control often should not trap focus like a modal dialog. It may still need:

  • a clear accessible relationship to the trigger
  • dismiss on outside interaction when appropriate
  • dismiss on Escape when appropriate
  • sensible focus movement if interactive content appears inside it

But turning every popover into a trapped mini‑dialog often makes the experience worse rather than better.

The interaction model matters here. A simple help tip, menu, date picker, and destructive confirmation are not all the same thing, even if they float over the page.


Background Content Should Stop Behaving as Active Content

With a true modal, background content should not remain available to keyboard users as if nothing happened.

That means more than a dim backdrop. We need to prevent focus from wandering behind the dialog and ideally prevent pointer interaction with background content as well.

This is one reason many teams choose a well‑tested overlay primitive library rather than building everything from scratch. Focus trapping, scroll locking, aria relationships, stacking, and nested overlays all contain edge cases that are easy to underestimate.


Do Not Forget Scroll and Viewport Behaviour

Accessibility is not only about screen readers and keyboard semantics.

On smaller screens, overlays can become difficult quickly if:

  • the body scrolls behind the modal
  • the drawer content cannot scroll independently
  • focus jumps to off‑screen content
  • the close action is difficult to reach at high zoom

Responsive overlay behaviour is part of the same accessibility story. A perfectly labelled dialog that becomes unusable at 400 percent zoom is still failing the user.


Good Semantics Depend on the Overlay's Job

This is the part worth slowing down for:

  • use dialog semantics for genuine dialogs
  • use menu semantics for actual menus
  • use disclosure behaviour for simple show and hide panels
  • use popover‑style behaviour when the interaction is lightweight and contextual

Trying to solve every floating UI pattern with one generic overlay usually produces a messy mixture of keyboard rules and aria roles.


If the Overlay is Not a Product Differentiator, Consider Proven Primitives

There is nothing noble about hand‑rolling every accessibility behaviour yourself if the application would be better served by stable, well‑tested primitives.

If the team does build its own components, the implementation should be treated as infrastructure rather than decorative UI. That means testing keyboard movement, focus return, screen reader naming, pointer dismissal, and stacked behaviour deliberately.


Useful References

Accessible overlays are rarely impressive because of one clever trick. They are impressive because all the small responsibilities are handled consistently: focus goes where it should, the right semantics are announced, the background behaves properly, and the user is not left to guess where they are after the interaction ends.

That is the standard worth aiming for. If the overlay works only in the mouse demo, it is not finished.


Have a complex web platform issue?

Tell me what is blocked, what has changed, and what needs to be true after the fix. I'll come back with a practical next step.