In an accounting product, a library date picker opened inside a record-payment dialog lets Tab escape to the page behind, and closing it loses focus. How should the library fix this?
answer
- each overlay rolled its own trap
- only the top layer is active
- remember who opened it
- everything beneath is inert
- Escape closes one layer
basics
~20 sReplace per-component traps with one shared focus-layer stack: only the top layer contains focus, everything beneath is inert, Escape closes one layer, and closing returns focus to the opener, or a logical fallback if it is gone.
solid answer
~50 sThe symptoms point to **each overlay managing focus on its own**. When the date picker opened, the dialog's containment was switched off or fought with the picker's — often because the picker is rendered outside the dialog's subtree — so Tab escaped; when the picker closed, nobody restored the dialog's containment or remembered where focus came from. The fix is a **shared focus-layer utility**: opening an overlay pushes a layer that records its opener and becomes the only active one; everything beneath, including the dialog, is inert; Escape closes only the top layer; popping it returns focus to its opener — or, per the Authoring Practices, to a logical element if the opener no longer exists — and reactivates the layer below. Layer membership is registered, not inferred from nesting. Built once, it fixes the dialog, the picker, the side sheet and every future overlay together.
go deeper
Recall the modal contract: focus moves in on open, Tab stays inside, Escape closes, focus returns to the opener.
Explain why overlays rendered outside their parent's subtree break per-component traps and why the page beneath must be inert.
Diagnose interacting overlays, design a registered layer stack with opener tracking and fallback, and plan the migration away from private traps.
Decide which focus behaviours the system centralises and locks down, and how web focus utilities map onto native platforms' presentation models.
## What the bug report describes In a small-business accounting product, a user opens **Record payment**, a modal dialog. Inside it, the payment-date field opens a **date picker** overlay. Two things go wrong: 1. While the picker is open, Tab eventually moves focus to the invoice list *behind* the dialog. 2. When the picker closes, focus lands at the top of the page instead of back on the date field. For a keyboard or screen-reader user, the first means working in content they cannot see; the second means starting over from the top of a long page. ## Why it happens The usual cause is that **each overlay implements its own focus trap**: - The dialog's trap watches its own subtree. The picker is often rendered at the document root to escape clipping, so when focus enters it, the dialog's trap sees focus *leaving* and either pulls it back or gives up. - The picker's trap activates and the dialog's trap is disabled to avoid fighting — but when the picker closes, nothing re-enables the dialog's trap. - Neither records **who opened it**, so on close focus falls to the default: the start of the page. - The page behind is not made **inert**. Even when Tab is contained, a screen-reader user moving through content in reading mode can still wander into the invoice list, because that navigation does not follow the Tab sequence. None of the three components is wrong on its own. The bug lives in their interaction, which is why fixing it component by component never ends. ## The contract the library must implement The Authoring Practices modal dialog pattern states the behaviour: - When a dialog opens, **focus moves to an element inside it**. - **Tab and Shift+Tab stay inside** the dialog, wrapping from last to first and back. - **Escape closes** the dialog. - Windows under a modal dialog are **inert** — users cannot interact with them. - When it closes, **focus returns to the element that invoked it**, unless that element no longer exists or the workflow makes another element more logical. This containment is not the keyboard trap that WCAG 2.2 success criterion **2.1.2 No Keyboard Trap** (Level A) forbids: that criterion allows standard exit methods, and Escape and the close button are exactly that. ## A shared focus-layer stack | Operation | What the utility does | |---|---| | Open overlay | records the opener, pushes a layer, makes everything outside the new top layer inert, moves focus inside | | Tab / Shift+Tab | wraps within the top layer only | | Escape | closes the top layer only, never the whole stack | | Close overlay | pops the layer, reactivates the one beneath, returns focus to the recorded opener or a logical fallback | Key design points: - **Registration, not nesting.** An overlay rendered elsewhere in the document still belongs to the layer that opened it, because it registered with the stack. - **One source of truth.** Components never trap focus themselves; they declare *I am a layer* and the utility enforces it. - **Fallback on close.** If the opener has gone — the row that opened it was deleted — focus goes to a logical neighbour, never to the page start. ## The other shared focus utilities The same reasoning applies to smaller cases, which is why libraries ship a small family together: - **Focus return** for any overlay, including non-modal popovers. - **Roving focus** for composites such as a toolbar or a tab list: one Tab stop for the group, arrows within it, and on re-entry either the first enabled control or the last-focused one, as the Authoring Practices toolbar pattern allows. ## Rolling the fix out 1. Build the stack and migrate the dialog, picker and side sheet first — they are where layers meet. 2. Keyboard-test the combinations, not just each overlay alone: dialog plus picker, sheet plus menu. 3. Remove every private trap so no component can fight the stack again. On native mobile the platform's modal presentation usually provides containment, but the library still owns which element receives focus on open and where it returns on close.
- Why is 'return focus to the opener' not enough on its own?The opener may no longer exist — the invoice row that opened the dialog may have been deleted by the action itself. The Authoring Practices then say to move focus to another element that provides a logical workflow. A utility that blindly restores to a missing element drops focus to the page start, recreating the original bug.
- Is a modal dialog that keeps Tab inside it a WCAG keyboard trap?No. Success criterion 2.1.2 No Keyboard Trap, at Level A, requires that focus can be moved away using the keyboard, with standard exit methods allowed. A dialog that closes on Escape and has a close control meets that. It becomes a failure only if there is no keyboard way out.
- What should Escape do when the date picker is open inside the dialog?Close only the picker and return focus to the date field, leaving the dialog open. Escape closes the top layer. Closing the whole stack at once would throw away the user's half-completed payment and dump focus back on the page.
saying these in an interview costs you the question
- Each overlay should implement its own focus trap for independence.
- A modal that keeps Tab inside it violates No Keyboard Trap.
- On close, focus can go to the top of the page.
- Escape should close every open layer at once.
- An overlay rendered outside the dialog's subtree is outside its focus scope.