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.
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()