patternnextjsModerate
Modal dialog opened by a link loses focus return when its trigger list is keyed per render
Viewed 0 times
React 19, Next.js 15+ App Router
showModal focus restorekey remountrouter.replace dialogWCAG focus orderurl driven panel
browserssr
Problem
In a server-rendered React page, a table row has a link that adds a URL parameter, and that parameter opens a native <dialog> with showModal(). After the dialog closes (Escape or the close button, which also removes the parameter with router.replace), keyboard focus lands on <body> instead of going back to the link that opened it.
Solution
Key the list or table by its data state (the list URL without the panel or dialog parameters), not by a per-render value such as Date.now(). The row elements then stay the same DOM nodes when only the dialog parameter changes, so the browser's built-in focus restoration (back to the element focused when showModal ran) works. Also keep the <dialog> always mounted and close it with close() when the URL no longer asks for it, rather than unmounting it.
Why
A native modal dialog restores focus to the element that was focused when showModal() was called. With a key that changes on every render, each navigation remounts the whole table, so the original trigger is gone and focus falls back to the document body.
Gotchas
- A per-render key is sometimes added to reset uncontrolled checkboxes or search inputs; keying by data state still resets them when the filters actually change
- Check focus after close with document.activeElement in a headless browser test
Code Snippets
Key by list state instead of render time
// before: <Table key={`selection-${Date.now()}`} rows={rows} />
<Table key={`selection-${listUrl(state)}`} rows={rows} />
<Dialog open={detailsId !== null} returnTo={listUrl(state)}>{/* ... */}</Dialog>Context
When a panel or dialog is opened by a URL parameter on a list whose rows remount on every render
Revisions (0)
No revisions yet.