Skip to content
All posts
Web Accessibility·17 September 2026

Accessible Modal Focus Management: React Dialog Guide

By Archebold Degraft Acquah

webscan.inclusiveappcraft.com

Small Code, Big Barrier

Your Modal Opens, but Keyboard Focus Never Enters It

The modal is open. Everyone looking at the screen can see it. But for a keyboard or screen-reader user, nothing may have happened at all.

Imagine you're filling out a form and activate a button labelled “Delete account.”

A confirmation dialog appears in the middle of the screen:

Delete your account?

This action cannot be undone.

Cancel
Delete account

Visually, the interaction is obvious. The background may dim, the dialog appears above everything else, and your attention immediately moves to it.

Now try the same interaction without looking at the screen.

Activate Delete account with your keyboard or screen reader.

The dialog appears.

But keyboard focus stays on the Delete account button behind it.

Press Tab.

Perhaps you move to another control behind the dialog.

Press Tab again.

Another background control.

The modal is visually telling one story:

“This dialog is now the active interface.”

The keyboard is telling another:

“Nothing changed. Keep navigating the page underneath.”

That's not just an inconvenience. The application's visual state and its programmatic interaction state have become disconnected.

And once again, a relatively small implementation mistake has created a significant accessibility barrier.

Opening a Modal Is More Than Making It Visible

Developers often think about dialogs primarily in visual terms.

Set some state:

const [isOpen, setIsOpen] = useState(false);

Render the component:

{isOpen && <DeleteAccountDialog />}

Add an overlay, position it in the centre of the viewport, give it a high z-index, and the modal is finished.

Visually, perhaps.

Accessibility requires more.

When a modal dialog opens, several things need to happen together:

  1. The dialog must be identified programmatically as a dialog.

  2. It needs an accessible name.

  3. Keyboard focus needs to move into it.

  4. Keyboard interaction needs to remain appropriately contained while it is modal.

  5. The user needs a keyboard-accessible way to dismiss it when dismissal is permitted.

  6. When it closes, focus usually needs to return somewhere logical — typically the control that opened it.

Missing any of these can create a confusing experience.

Let's look at the focus problem first.

The First Problem: Focus Never Enters the Dialog

Consider this:

<button onClick={() => setIsOpen(true)}>
  Delete account
</button>

{isOpen && (
  <div className="modal">
    <h2>Delete your account?</h2>
    <p>This action cannot be undone.</p>

    <button>Cancel</button>
    <button>Delete account</button>
  </div>
)}

Clicking the button renders the dialog.

But rendering something does not automatically move keyboard focus to it.

The browser doesn't know that your newly rendered <div> represents a new interaction context.

So focus may remain on the button that opened the dialog.

For a screen-reader user, this can be particularly confusing. New content has appeared elsewhere in the DOM, but their current reading and interaction position hasn't necessarily moved with it.

The application changed.

The user's context didn't.

Move Focus Into the Dialog

When the dialog opens, focus should generally move to an appropriate element inside it.

Exactly which element should receive initial focus depends on what the dialog contains.

For a simple confirmation dialog, it might be one of the action buttons.

For other dialogs, focusing the heading or another static element can sometimes provide better context before the user encounters the controls.

For example:

const headingRef = useRef<HTMLHeadingElement>(null);

useEffect(() => {
  if (isOpen) {
    headingRef.current?.focus();
  }
}, [isOpen]);

Then:

<h2 ref={headingRef} tabIndex={-1}>
  Delete your account?
</h2>

Just like the route-focus technique discussed in the previous Small Code, Big Barrier article, tabIndex={-1} allows us to focus an element programmatically without adding it to the normal Tab sequence.

When the dialog opens, focus moves to its heading.

A screen reader now has meaningful context:

“Delete your account?”

The user knows something happened and understands what interface they have entered.

But we're not finished.

The Second Problem: Focus Escapes Behind the Modal

Suppose focus successfully enters the dialog.

The dialog contains:

  • Cancel

  • Delete account

The user presses Tab.

Focus moves between the controls.

Then they press Tab again.

Suddenly, focus moves to a link in the page behind the dialog.

The modal is still visually open.

Now we have another mismatch.

A modal dialog communicates that interaction with the rest of the interface is temporarily unavailable. If keyboard users can continue tabbing through controls behind it, that model has been broken.

For a sighted keyboard user, this can be disorienting because the visible focus indicator may disappear behind the overlay.

For a screen-reader user, it can be even stranger. They may suddenly encounter navigation links, form controls, or other content that appears unrelated to the dialog they're currently using.

If the dialog is genuinely modal, background content should not remain normally interactive.

Don't Build a Focus Trap by Accident

You will often hear this described as trapping focus.

The idea is that while a modal is active, keyboard interaction should stay within the modal's relevant interactive context rather than wandering into the application underneath.

But there is an important implementation lesson here:

Don't immediately respond by writing a huge pile of custom keyboard code.

Modern platform features and well-tested accessible component libraries can handle much of this behaviour more reliably than a home-grown implementation.

Whenever possible, start with native HTML.

Meet the Native <dialog> Element

HTML provides a <dialog> element specifically for dialogs.

For example:

<dialog>
  <h2>Delete your account?</h2>
  <p>This action cannot be undone.</p>

  <button>Cancel</button>
  <button>Delete account</button>
</dialog>

When opened as a modal using:

dialog.showModal();

the browser provides important modal behaviour that is difficult to reproduce correctly from scratch.

This doesn't mean <dialog> makes every accessibility decision for you. You still need meaningful content, sensible initial focus, accessible controls, appropriate dismissal behaviour, testing, and focus restoration.

But native semantics give you a much stronger foundation than beginning with:

<div class="modal">

and attempting to reconstruct dialog behaviour manually.

This is an accessibility principle worth remembering:

Before adding ARIA and JavaScript to recreate a platform control, check whether HTML already provides the control you need.

If You Build a Custom Dialog, Semantics Matter

Sometimes a custom implementation is necessary.

In that case, simply styling a <div> to look like a modal doesn't tell assistive technology what it is.

A custom dialog might require semantics such as:

<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="dialog-title"
>
  <h2 id="dialog-title">Delete your account?</h2>

  <p>This action cannot be undone.</p>

  <button>Cancel</button>
  <button>Delete account</button>
</div>

Here, role="dialog" communicates the type of interface.

aria-modal="true" communicates that it is modal.

And aria-labelledby="dialog-title" gives the dialog an accessible name using its visible heading.

But this is where developers need to understand an important distinction.

ARIA communicates semantics. It does not automatically implement behaviour.

Adding role="dialog" does not automatically move focus.

Adding aria-modal="true" does not magically repair incorrect keyboard behaviour.

Your implementation still has to behave like the thing you claim it is.

If you tell assistive technology that something is a modal dialog, your interaction model needs to support that claim.

The Third Problem: The Dialog Closes and Focus Disappears

Now suppose we've solved opening the modal.

Focus moves into it.

Keyboard interaction stays within the modal.

The user activates Cancel.

The modal disappears.

Where does focus go?

This is another point where implementations often fail.

If the currently focused element is inside a component that gets removed from the DOM, the browser can no longer keep focus on that element.

Focus may effectively fall back to the document.

The keyboard user now has to figure out where they are again.

Think about what should happen logically.

The user was here:

Delete account button

They opened a temporary interface.

They decided:

Cancel.

The temporary interface disappears.

The logical place to return them is usually the control that opened it:

Delete account button

That restores their previous context.

Remember the Trigger

In React, you might keep a reference to the trigger:

const triggerRef = useRef<HTMLButtonElement>(null);

Attach it:

<button
  ref={triggerRef}
  onClick={() => setIsOpen(true)}
>
  Delete account
</button>

Then, after closing the dialog:

triggerRef.current?.focus();

The interaction becomes:

Trigger → dialog → interaction → close → trigger

That's predictable.

Predictability matters enormously in keyboard accessibility.

What If the Trigger No Longer Exists?

Focus restoration should also respect what happened inside the dialog.

Imagine the modal isn't simply a confirmation box.

Perhaps it deletes an item from a list.

The user activates:

Delete project

The confirmation dialog opens.

They confirm deletion.

The project disappears.

The original Delete project button no longer exists.

Trying to return focus to it makes no sense.

In that situation, choose the next logical location.

Perhaps focus should move to:

  • the next project;

  • the previous project;

  • the list heading;

  • a success message; or

  • another meaningful location related to the completed action.

Accessibility isn't about blindly following:

trigger.focus();

It's about preserving the user's context.

The correct question is:

After this action, where would a keyboard or screen-reader user logically expect to be?

Don't Forget Escape

For dialogs that users are allowed to dismiss, the Escape key is an important convention.

A user shouldn't necessarily have to Tab around searching for a Close or Cancel button just to leave a temporary interface.

However, Escape behaviour needs to match the purpose of the dialog.

Not every modal should be dismissible without completing a required decision or process.

The key principle is consistency:

If a dialog can be dismissed, provide clear, operable dismissal mechanisms and ensure keyboard users can use them.

And if Escape closes it, focus restoration still matters.

Closing the modal correctly but leaving focus nowhere useful only solves half the problem.

What Should Receive Initial Focus?

There isn't one universal answer.

Consider a simple informational dialog:

Session expiring

Your session will expire in two minutes.

Continue session

Moving directly to the action may be reasonable.

Now consider a destructive confirmation:

Delete all customer records?

This action permanently removes 4,312 records and cannot be undone.

Cancel
Delete permanently

Automatically placing focus on Delete permanently may be a poor choice.

A user pressing Enter reflexively could trigger a destructive action.

Depending on the dialog's content and complexity, initial focus may be better placed on:

  • the dialog heading;

  • a non-destructive action such as Cancel;

  • the first relevant form field; or

  • another logical starting element.

The purpose is not simply to satisfy a rule saying:

Focus must be inside the dialog.

The purpose is to give the user a safe and understandable starting point.

Screen-Reader Testing Changes How You Think About Modals

A modal can look flawless while being almost unusable non-visually.

That's why I recommend testing it without relying on sight.

Open the application with a screen reader.

Navigate to the trigger.

Activate it.

Then ask:

What was announced?

Do you know a dialog opened?

Do you know its name?

Where did focus move?

Can you understand the dialog before taking action?

Press Tab.

Does focus remain where it should?

Try Shift+Tab.

Can you move backwards predictably?

Close the dialog.

Where did focus return?

Then test again using only the keyboard.

And don't stop with desktop testing.

Screen readers on touch devices interact with interfaces differently from desktop keyboard-and-screen-reader combinations. A component that feels acceptable with one assistive technology may expose problems with another.

Why a Snapshot Scanner Can Miss This Too

Notice how many problems we've discussed that depend on time and interaction:

The modal opens, but focus doesn't move.

Focus enters, but later escapes.

The dialog closes, but focus isn't restored.

Escape doesn't work.

The trigger disappears after an action and focus restoration breaks.

Those aren't simply questions about what HTML exists.

They're questions about what happens:

before → during → after an interaction.

A static accessibility scan might correctly identify missing dialog semantics.

It might find an invalid ARIA attribute.

It might identify an unnamed button.

Those checks matter.

But a DOM snapshot taken after the modal opens cannot, by itself, tell the complete story of how focus travelled through the interaction.

That's why accessibility testing needs to include behaviour.

Where Webscan Fits

This is precisely the kind of distinction we're building Webscan around.

Automated page scanning is important, but accessibility doesn't stop at static rule violations.

Real interfaces have states.

They have transitions.

They have user journeys.

A user opens a menu, launches a modal, submits a form, encounters an error, closes a dialog, changes routes, and continues working.

Those transitions are where many accessibility barriers live.

Webscan is designed to combine automated accessibility checks with real-browser journeys and session-based testing so behavioural problems have a better chance of becoming findings rather than disappearing between snapshots.

And when a barrier is identified, the workflow shouldn't stop at reporting it.

The objective is:

Find it → Fix it → Verify it.

That's the difference between treating accessibility as a score and treating it as an engineering practice.

Explore Webscan by Inclusive App Craft to identify accessibility barriers, investigate real user journeys, remediate issues and verify that fixes actually work.

A 60-Second Modal Test

You can test a modal in your application without installing anything.

Use only your keyboard.

Navigate to the button that opens the modal and press Enter or Space.

Once the modal appears, ask:

  1. Where is keyboard focus?

  2. Can I tell that I've entered the dialog?

  3. Does Tab move through the dialog logically?

  4. Can focus escape into the page behind it?

  5. Does Shift+Tab work predictably?

  6. If the dialog is dismissible, can I close it with the expected keyboard interaction?

  7. After closing it, where does focus go?

Then repeat the test with a screen reader.

If the dialog appears visually while keyboard focus remains somewhere behind it, you've found the first barrier.

If focus enters but later escapes behind the modal, you've found another.

If closing the modal leaves the user somewhere unexpected, you've found a third.

The component may still look perfect throughout all three.

That's the point.

Accessibility is a practice, not a score.


Developer Checklist

Before shipping a modal dialog, verify that:

  • the component uses appropriate native HTML or correct dialog semantics;

  • the dialog has a meaningful accessible name;

  • focus moves to a sensible location when it opens;

  • modal interaction does not leave keyboard users navigating background controls;

  • keyboard navigation works forwards and backwards;

  • dismissal works appropriately, including Escape where applicable;

  • focus returns to a logical location when the dialog closes;

  • destructive actions do not receive careless initial focus;

  • dynamic changes inside the dialog are communicated when necessary; and

  • the complete interaction has been tested with a keyboard and screen reader.

A modal isn't accessible simply because a screen reader can find it in the DOM.

It is accessible when the entire interaction makes sense without relying on sight or a mouse.

Was this article helpful?

Share this article

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment

Comments are moderated before they appear.