How to Build an Accessible Modal From Scratch

A modal is one of the most commonly broken accessible patterns on the web, and one of the most reachable to get right.

How to Build an Accessible Modal From Scratch — Troiana insight cover

In short

An accessible modal needs a focus trap that keeps keyboard navigation inside it, focus moved to it on open and returned to the trigger on close, Escape-to-close support, correct ARIA roles (role="dialog", aria-modal="true"), and background content marked inert so screen readers ignore it while the modal is open.

Why modals are so commonly broken

A modal looks simple — an overlay, a box, a close button — but correct behavior touches focus management, keyboard interaction, and screen reader semantics simultaneously, and getting any one of them wrong breaks the experience for keyboard and screen reader users even when it looks fine visually. This is exactly why it's worth building (or verifying) carefully rather than assuming a quick implementation is fine.

Moving focus on open

When a modal opens, focus needs to move into it — typically to the modal's heading or its first focusable element. Without this, a keyboard or screen reader user has no indication the modal even opened; they're still "standing" wherever they were on the page behind it.

Trapping focus inside

While the modal is open, Tab and Shift+Tab should cycle only through focusable elements inside the modal — never escaping to the page behind it. This means intercepting Tab at the last focusable element to wrap back to the first (and Shift+Tab at the first to wrap to the last), and typically requires manual implementation unless using a library or the native <dialog> element, which handles this natively.

Returning focus on close

When the modal closes, focus should return to whatever element triggered it — usually the button that opened the modal. Without this, a keyboard user closes the modal and finds focus has silently reset to the top of the page, losing their place entirely.

Making background content inert

While the modal is open, content behind it should be excluded from both keyboard tabbing and screen reader navigation — the native inert attribute on the background container handles both in one step in modern browsers. Without it, a screen reader user can navigate "through" content that's visually hidden behind an overlay, which is deeply confusing.

The ARIA roles that matter

  • role="dialog" (or role="alertdialog" for something requiring immediate attention) on the modal container.
  • aria-modal="true" to signal that everything outside it is inert.
  • aria-labelledby pointing to the modal's heading, so screen readers announce what the dialog is for.

Escape key and click-outside behavior

Escape should close the modal — this is expected keyboard behavior users rely on. Clicking the overlay outside the modal content is a common convenience for closing too, but it should never be the only way to close for keyboard users; always provide a visible, focusable close button as well.

Consider the native <dialog> element

Modern browsers support a native <dialog> element with showModal() that handles focus trapping and some ARIA semantics automatically, reducing the amount of manual work needed. It's worth evaluating before building the full pattern from scratch — native browser behavior is often more robust than a custom reimplementation, especially for edge cases like nested dialogs.

A testing checklist

Open the modal with a mouse, then try navigating and closing it using only a keyboard — Tab through it, confirm focus never escapes, press Escape, confirm focus returns to the trigger. Then test with a screen reader to confirm the dialog role and label are announced correctly. If all of that works, the modal is accessible; if any step fails, that's exactly where a real user would get stuck.

Keep reading: How to Ship a Static Site With Zero Build Step · How to Build a Color System With Design Tokens.

For the underlying standards, see the W3C Web Accessibility Initiative.

Common questions

Does the native <dialog> element handle accessibility automatically?

It handles a meaningful portion — focus moving into the dialog and some focus trapping — but you should still verify labeling, Escape handling, and focus return to the trigger explicitly rather than assuming everything is automatic.

What happens if focus isn't trapped inside an open modal?

A keyboard user can Tab past the modal into content behind it that's visually obscured, becoming lost or confused about what's actually interactive on the page.

Is click-outside-to-close accessible on its own?

No — it's a convenience for mouse users, but a modal must always also provide a visible, keyboard-focusable close button, since click-outside isn't a reliable or expected keyboard interaction.

Have something worth building right?