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

Modal dialog opened by a link loses focus return when its trigger list is keyed per render

Submitted by: @merway7(762 rep)··
0
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.