Field Notes

Where focus goes when a dialog opens

A dialog owes a keyboard user three things. Put focus inside it on open. Keep Tab inside while it is open. Give focus back to the opener on close. Miss any one and the reader is lost behind the dialog or dropped at the top of the page.

Published

  • keyboard
  • focus
  • dialogs
  • wcag 2.2

A dialog is a page inside a page, and focus has to cross the border twice. Going in, it has to land somewhere useful. While inside, it has to stay inside, because everything behind the dialog is supposed to be inert. Coming out, it has to go back to where it was. Each crossing is a place to get it wrong, and the three mistakes feel different to the person on the keyboard.

Open: put focus inside

When the dialog opens, move focus to the first thing a reader would want: the first field, or the first button when there are no fields, or the dialog itself when it is mostly text. Without this, focus stays on the button that opened the dialog. The next Tab goes to whatever follows that button in the page, behind the dialog, and the reader has to hunt for a way in. A screen reader user may not know a dialog opened at all.

A dialog whose first control is the Close button is a common half-fix. It is inside, so it counts, but the first press lands on the exit. Prefer the first field.

Open: keep Tab inside

While the dialog is open, Tab from the last control wraps to the first, and Shift+Tab from the first wraps to the last. Nothing behind the dialog is reachable. The native dialog element does this when opened with showModal(), and the inert attribute on the rest of the page does it for a custom one. Scripts that catch Tab on the first and last control also work, which is what the lab does.

Without a trap, focus leaves the dialog on the next press, lands on something the reader cannot see, and the page starts responding to keys in two places at once.

Close: give it back

On close, whether by the Close button, by Escape, or by finishing the task, move focus back to the element that opened the dialog. That is the one place the reader knows. Without it, focus goes to the top of the document, and the reader has to Tab all the way back down to find where they were. On a long page that is dozens of presses.

Remember the opener before you open, and focus it after you close. If the opener is gone (a Delete button that removed its own row), focus the nearest sensible thing: the next row, or the heading of the list.

Escape closes

Escape closes a dialog. Every reader expects it, and showModal() gives it for free. A custom dialog needs a keydown handler. A dialog that cannot be closed from the keyboard is a trap with no exit, which is success criterion 2.1.2, and the ruleset's AW-KEY-004 covers the close control and the escape key together.

The three, as a table

Moment What to do What goes wrong without it
Open Focus the first field, or the dialog Next Tab lands behind the dialog
Open Keep Tab inside; make the rest inert Focus escapes to an unreachable page
Close Focus the opener Reader dropped at the top of the page

AW-KEY-004 already asks for the close control, Escape, the trap, and the return. AW-FOCUS-005, added with this note, is the testable half: script the open, read where focus is, script the close, read where focus is. Corpus page 36 plants a dialog that gets the first two right and the return wrong.

The native element does most of this

<button type="button" id="open">Delivery address</button>
<dialog id="address" aria-labelledby="address-title">
  <h2 id="address-title">Delivery address</h2>
  <label for="name">Name</label>
  <input id="name" autofocus>
  <button type="button" id="close">Close</button>
</dialog>
<script>
  const dialog = document.getElementById('address');
  document.getElementById('open').addEventListener('click', () => dialog.showModal());
  document.getElementById('close').addEventListener('click', () => dialog.close());
</script>

showModal() makes the rest of the page inert, traps Tab, closes on Escape, and returns focus to the opener on close(). autofocus on the first field puts focus inside on open. Four lines of script, and the three crossings are handled. A custom dialog has to do each of them by hand, and the lab shows what each one costs when it is skipped.

Try it

In the Tab Walk lab, the third section is a dialog with the three behaviours as checkboxes, all on. Open it, press Tab a few times, press Escape, and read where focus is. Then untick one box at a time and do it again.

Sources

  • w3.org, Understanding Success Criterion 2.4.3: Focus Order, the dialog examples
  • w3.org, Understanding Success Criterion 2.1.2: No Keyboard Trap
  • developer.mozilla.org, the dialog element and showModal()

Open the companion lab

  • Tab order is a promise the DOM makes

    The Tab key follows the HTML, not the layout. CSS cannot change that order, a positive tabindex hijacks it, and aria-hidden does not remove a stop. One real walk found 162 stops where a list of the markup said 64. Here is why, and how to check.

  • The sticky header that eats your focus

    Shift+Tab back up a form and the field you land on slides under the sticky header. The browser scrolled just far enough, and just far enough is behind the bar. WCAG 2.2 calls it focus not obscured. One CSS property fixes it.

  • The focus ring is the interface

    For anyone who moves through a page without a mouse, the focus ring is the cursor. Here is what a great one looks like and why most of them fail.

Spotted something this note gets wrong?

Corrections and better examples are the best kind of message.

Send a correction