Greta.sh

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.

Modal focus test matrix
Dialog typeInitial focus (expected)Tab and Shift+TabEscapeFocus after close
Edit recordFirst form fieldWraps inside the dialogCloses, or asks before discarding changesThe Edit button that opened it
Delete confirmationCancel, the least destructive actionWraps inside the dialogCloses without deletingCancel: the trigger. Delete: a specified next element, such as the next row's action button
Sign-in dialogEmail fieldWraps inside the dialogCloses; the user stays signed outThe control that required sign-in
Long details or termsThe dialog title, made focusable with tabindex='-1'Wraps inside the dialogClosesThe link or button that opened it
Date picker in a dialogThe selected date, or todayWraps inside the dialogCloses without changing the valueThe date field or its button
Nested confirmationThe least destructive optionWraps inside the top dialog onlyCloses the top dialog onlyBack into the dialog underneath
Unsaved-changes prompt on leaving a pageStay on this pageWraps inside the dialogKeeps the user on the pageThe 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.

Continue with Greta.sh

Explore Greta →