Skip to content

A modal opens while focus stays behind it

The dialog is visible, but keyboard navigation still follows the obscured page.

WCAG 2.4.3WCAG 2.1.2 Test with KeyboardScreen readerZoomForced colors Markdown for agents
Broken Focus stays behind.
Fixed Focus moves inside.
The same control broken and fixed.
01

The problem

The dialog is on screen, but the keyboard is still on the page behind it.

What happens Tab walks the page behind the dialog. Read moreRead less

Activate Edit details. A dialog covers the page. Press Tab: focus moves through navigation links behind the overlay instead of entering the dialog.

Why it fails It looks modal. Nothing makes it behave so. Read moreRead less

The visual layer changed without establishing a modal interaction. The user can reach hidden background actions and may not encounter the dialog heading.

Who is affected Keyboard and screen-reader users, in the wrong context. Read moreRead less

A keyboard or screen-reader user may continue editing the wrong context, miss required input, or lose their place after closing.

What automation and AI miss role and aria-modal do not move focus. Read moreRead less

Correct role and aria-modal attributes do not move focus or prevent background interaction. A static DOM snapshot cannot prove opening, containment, dismissal, and restoration across the actual workflow.

02

Try it

Open the dialog, then press Tab.

Open the dialog, then press Tab and Escape.

A recreated demo. The broken version is intentionally inaccessible.

03

The fix

Open a native dialog with showModal(), and check where focus goes on close.

  1. 1Give the dialog a name with aria-labelledby.
  2. 2Put initial focus where the task starts, or on the least destructive action.
  3. 3Return focus to the trigger, or to a sensible place if it is gone.
Before An overlay that only looks modal

html

<div class="overlay" hidden>  <div class="modal">    <p class="modal-title">Edit details</p>    <button onclick="closeModal()">Close</button>  </div></div>

js

function openModal() {  document.querySelector('.overlay').hidden = false;}
After A native dialog

html

<button type="button" id="edit-trigger">Edit details</button> <dialog id="edit-dialog" aria-labelledby="edit-title">  <h2 id="edit-title">Edit details</h2>  <form method="dialog">    <label for="display-name">Display name</label>    <input id="display-name" name="name">    <button value="save">Save</button>    <button value="cancel">Cancel</button>  </form></dialog>

js

const trigger = document.getElementById('edit-trigger');const dialog = document.getElementById('edit-dialog');trigger.addEventListener('click', () => dialog.showModal());

js

// If the trigger no longer exists after closing, choose where focus goes.dialog.addEventListener('close', () => {  if (!trigger.isConnected) document.getElementById('item-list')?.focus();});
Why this works

Showing the overlay changes what people see, nothing else. Focus stays on the trigger, Tab continues through the page behind, Escape does nothing, and the dialog has no name.

showModal() moves focus into the dialog, makes the page behind it inert, and closes the dialog on Escape. Buttons in a method="dialog" form close it too. Current browsers return focus to the element that opened it; still verify this, because a re-rendered page may have replaced that element.

Implementation decisions

A long dialog may need initial focus on a heading with tabindex="-1" so its beginning stays visible. Destructive confirmations should put initial focus on the least destructive action; use autofocus on that button. Avoid nesting dialogs until their restoration behavior is tested.

If you cannot use the native element, a custom modal needs the same behavior: role="dialog" with aria-modal="true" and a name, focus moved in on open, the rest of the page made inert, Escape to close, and focus returned on close.

04

Verify the fix

5 checks, no mouse.

0 of 5 checked

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the shared dialog component changes.

Limits

This is a starting pattern, not a guarantee for every application. The snippets omit saving and validation. Test the complete journey with your target assistive technology.

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

References: www.w3.org

Keep this fixed as your code changes.

Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.

How retests work