Implementation worksheet · 7 min read
How to Test Modal Focus in an AI-Generated Interface
Test every modal in four moves. Open it from the keyboard and check that focus lands inside it. Tab and Shift+Tab past both ends and check that focus wraps inside rather than escaping to the page behind. Press Escape and check that it closes. Then check where focus goes: back to the control that opened it, or, when that control no longer exists because the dialog deleted its row, to a sensible next element you have specified. The WAI-ARIA Authoring Practices dialog pattern describes this behaviour, and it supports WCAG 2.2 criteria including 2.1.2 No Keyboard Trap, 2.4.3 Focus Order and 4.1.2 Name, Role, Value. Run the test once per kind of dialog the app contains, not once for 'the modal'.
Scope: the actor is a tester or builder checking dialogs in an AI-generated app: an edit-record dialog, a delete confirmation, a sign-in dialog, and any dialog opened from inside another. The starting state is a deployed preview with disposable synthetic records, a keyboard and, for one step, a screen reader. The boundary is the dialog and the control that opened it; the page behind matters only because it must be unreachable while the dialog is open. The outcome is a pass or fail per dialog per move, with the element focus actually landed on written down.
Put it into practice
1. List dialogs by job, not by component
A generated app may reuse one modal component for very different jobs. List them by what they do: edit a record, confirm a destructive action, sign in, pick a date, show long details. Each job has a different correct starting point for focus, so a pass on one doesn't cover the others.
2. Check where focus lands when it opens
Open the dialog with Enter on its trigger. Focus should move inside. The APG pattern says initial focus generally goes to the first focusable element, but suggests a static element at the top (such as the title, given tabindex='-1') when the content is long, and the least destructive action when the step is hard to undo. For a delete confirmation, that means Cancel, not Delete.
3. Tab past both ends
From the last control, Tab should wrap to the first control in the dialog; from the first, Shift+Tab should wrap to the last. If focus reaches the page behind, the dialog isn't containing its tab sequence and a keyboard user can operate content they can't see. The APG also warns to set aria-modal='true' only when the code really prevents interaction outside the dialog and the styling obscures it, because it can hide the rest of the page from some screen readers.
4. Close it every way it can close
Escape, the visible close or Cancel button, and the action that completes the task. The APG strongly recommends a visible button that closes the dialog in its tab sequence. A dialog with no keyboard route out at all fails 2.1.2 No Keyboard Trap (A). Escape doing nothing while a Close button is reachable is a departure from the APG pattern rather than a 2.1.2 failure on its own, and it is still worth filing.
5. Check where focus goes after it closes
Normally back to the control that opened the dialog, so the user carries on from where they were. The APG allows a different target when the opening control no longer exists, or when the workflow makes another element the logical next step. Delete is the case to test on purpose in a generated CRUD screen: the row and its button are gone. If nobody specified a target, check whether focus has fallen back to the top of the document, then specify one, such as the next row's action button or the table heading when no rows remain.
6. Listen to one open and close with a screen reader
Turn on VoiceOver (Command-F5 on a Mac) or NVDA on Windows, then open the dialog. You should hear its role and a name, taken from a visible title referenced by aria-labelledby or from aria-label. A dialog container with no dialog role or no accessible name fails 4.1.2 Name, Role, Value (A), which applies to components generated by scripts.
7. Check nested dialogs and sticky footers
When an edit dialog opens a 'Discard changes?' confirmation, Escape should close only the top dialog and focus should go back into the edit dialog, not to the page. In a tall dialog with a sticky footer, Tab through every field and check none is entirely hidden behind the footer when focused (2.4.11 Focus Not Obscured (Minimum), AA).
8. Worked example (illustrative, synthetic data)
An invoices screen in a generated billing app with three dialogs. Edit invoice INV-0042: opens with focus in the Customer field; Tab from Save wraps to the close button; Escape closes it and focus returns to the Edit button on that row. Delete invoice INV-0042: opens on Cancel; Enter on Delete removes the row, and the specified target is the Edit button for INV-0043. Discard changes, opened by pressing Escape in a modified edit dialog: opens on Keep editing; Escape closes only the confirmation and focus returns to the field being edited. In this synthetic run the delete dialog passed every move except the last: focus fell to the top of the page, so a keyboard user had to Tab through the whole sidebar to carry on.
Modal focus test matrix
Copy this structure into your review document and record your observed result for each row.
| Dialog type | Initial focus (expected) | Tab and Shift+Tab | Escape | Focus after close |
|---|---|---|---|---|
| Edit record | First form field | Wraps inside the dialog | Closes, or asks before discarding changes | The Edit button that opened it |
| Delete confirmation | Cancel, the least destructive action | Wraps inside the dialog | Closes without deleting | Cancel: the trigger. Delete: a specified next element, such as the next row's action button |
| Sign-in dialog | Email field | Wraps inside the dialog | Closes; the user stays signed out | The control that required sign-in |
| Long details or terms | The dialog title, made focusable with tabindex='-1' | Wraps inside the dialog | Closes | The link or button that opened it |
| Date picker in a dialog | The selected date, or today | Wraps inside the dialog | Closes without changing the value | The date field or its button |
| Nested confirmation | The least destructive option | Wraps inside the top dialog only | Closes the top dialog only | Back into the dialog underneath |
| Unsaved-changes prompt on leaving a page | Stay on this page | Wraps inside the dialog | Keeps the user on the page | The link or control that triggered it |
A failure worth checking
The delete confirmation that passed because Delete was never pressed. It handles initial focus, Tab and Escape correctly, so the tester marks it green. Nobody confirms the delete, because that would destroy the test record, and that path is the only one where the trigger disappears and focus has nowhere to go. Keep a disposable synthetic record for each run so the destructive path is exercised. The counterexample to over-trapping: a non-modal panel such as a filter drawer or chat widget that leaves the page usable shouldn't trap focus at all. Giving it modal behaviour and aria-modal hides the page from screen reader users while sighted users can still use it, which is the mismatch the APG warns about.
Common questions
Does WCAG require focus to return to the trigger?
Not in those words. WCAG 2.2 requires, among other things, that focus can be moved away from any component with the keyboard (2.1.2) and that focus order preserves meaning and operability (2.4.3). Returning focus to the trigger is how the WAI-ARIA Authoring Practices dialog pattern handles it, and it is what users of other apps will expect, so test for it and treat a different target as a defect unless it is specified for a reason.
What if the modal comes from a component library?
Test it anyway. A library can implement the pattern correctly and still be configured wrongly, with the close button hidden or initial focus overridden. Write the library and version next to the result so that an upgrade prompts a re-test.
How do I word the fix request?
Name the dialog, the move and the expected element: 'After Delete is confirmed in the invoices table, move focus to the Edit button of the next row, or to the table heading if no rows remain. Don't change the dialog's layout.' One dialog type per request, then re-run the matrix.
Basis and scope
This is a proposed implementation method using illustrative examples, not a measured benchmark or a customer case study. Prepared with AI assistance. Validate product-specific behavior against current documentation and your own test environment.