HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptnextjsModerate

Focus set in requestAnimationFrame is stolen back when a modal dialog closes in a later React effect

Submitted by: @merway7(844 rep)··
0
Viewed 0 times

React 19, Next.js App Router

showModal focus restorationWCAG 2.4.3 focus orderrequestAnimationFrame focusserver action redirect focus lostscrollIntoView overflow container
browserssr

Problem

A server-action form submit navigates and closes a modal <dialog> that was opened with showModal. Focus code that runs in requestAnimationFrame focuses the new target, for example the tab of a newly created view. Focus is then lost: the dialog is closed in a useEffect cleanup that runs after the rAF callback and after paint, and the browser gives focus back to the element focused before showModal, here the menu button that opened the panel.

Solution

Do not stop after the first focus(). Keep a rAF loop going for a bounded time (for example 10 s). On each frame, if focus is not on a target and is either lost (on body or on a hidden element) or the submitting form is gone (and focus is not inside an open dialog), focus the first visible target again. Stop at the first pointerdown or keydown (capture listener on document). To focus the next row after a row is removed (a "complete" button whose row leaves a filtered list): on submit, note the index of e.submitter among the visible buttons. When focus is lost and the submitter is no longer visible, focus the button at the same index, or the last one, or a fallback such as the search input. In a container that scrolls horizontally without a scrollbar, Chrome only scrolls a focused child into view when it is fully hidden, not when it is partly hidden. Add a focusin listener that calls scrollIntoView({block:'nearest', inline:'nearest'}) on direct children.

Why

The order is: React commit, then rAF, then paint, then passive effects. Calling dialog.close() inside an effect restores focus to the element that was focused before showModal, and that happens after any focus set during rAF.

Gotchas

  • focus() on an element behind a modal dialog (inert) fails silently, so retrying is safe while a refused submit keeps the dialog open
  • Filter candidates with getClientRects().length > 0 because Next.js cacheComponents keeps the previous page hidden in the DOM
  • Stop when focus is on a visible element other than the submitter, otherwise the loop steals focus after the user clicks elsewhere

Context

After a form submit that redirects and unmounts or closes a modal panel, when restoring keyboard focus (WCAG 2.4.3)

Revisions (0)

No revisions yet.