Focus After Filtering Removes the Focused Row

Permalink to "Focus After Filtering Removes the Focused Row"

A user focuses row 14, then something removes it: a filter they changed in a side panel, a live update that marks the record closed, a search that no longer matches. If nothing handles it, focus falls to the document body. Screen reader users are thrown to the top of the page; keyboard users must tab from the start. Data apps need a consistent rule for where focus goes when the focused row vanishes, and a short message explaining why.

This guide belongs to focus management in single-page apps.

Spec reference

Permalink to "Spec reference"
  • SC 2.4.3 Focus Order (A) — focus moves in a meaningful order; losing focus to the body breaks it.
  • SC 3.2.2 On Input (A) — changes of context on input need warning; moving focus far away can count.
  • SC 4.1.3 (AA) — explain removals with status messages.
  • SC 2.4.7 (AA) — the new focus target is visible.
Choosing a focus target when a row disappears Flow: the focused row is removed; if a row exists at the same index, focus it; otherwise the previous row; otherwise the results heading; announce the change. Choosing a focus target when a row disappearsFocused row removedrow 14 of 40Same index exists?focus new row 14Else previous rowrow 13Else results heading"No results" headingAnnounce"INV-1042 no longer matches. 39 results."
A fixed priority order keeps behaviour predictable.

When to use — and when not to

Permalink to "When to use — and when not to"

Apply this rule wherever rows can disappear while focused: filtering, search-as-you-type, live data, deletion by others, and pagination changes.

When the user is interacting with the filter control itself, focus should stay on that control; the focused row only matters when focus was in the table. The misapplication: always moving focus to the table after filtering, which pulls users out of the filter they are typing in.

Focus outcomes after a row disappears Comparison: letting focus drop to the body or jumping to the top loses context; a priority rule keeping focus near the old position preserves it. Focus outcomes after a row disappears✗ Focus to bodyJump to first rowSilent changeFocus pulled from filter input✓ Nearest surviving rowSame index or previous row"No longer matches" messageFocus stays in filter

Annotated code example

Permalink to "Annotated code example"
function applyFilter(newRows) {
  const active = document.activeElement;
  const inTable = table.contains(active);
  const oldIndex = inTable ? rowIndexOf(active) : -1;
  const oldId = inTable ? rowIdOf(active) : null;

  render(newRows);                                                    // re-render

  if (!inTable) return;                                               // user is in the filter: leave focus alone
  if (newRows.some(r => r.id === oldId)) return focusRowById(oldId);  // row survived
  const target = rowAt(oldIndex) || rowAt(oldIndex - 1) || resultsHeading;
  if (target === resultsHeading) resultsHeading.tabIndex = -1;
  target.focus();
  status.textContent = `${label(oldId)} no longer matches. ${newRows.length} results.`;
}

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Situation Focus after Announcement
Typing in filter, rows change Stays in filter input Result count
Focus in table, row survives Same row None or count
Focus in table, row removed Same index or previous row “INV-1042 no longer matches. 39 results.”
All rows removed Results heading “No results”
Live update closes focused row Next row “INV-1042 closed by another user”
Live update removes the focused row Sequence: a server update closes the focused invoice; the app computes the next target; focus moves to the row now at the same position; the status region explains why. Live update removes the focused rowServerAppTableStatusINV-1042 status → closedremove row, renderfocus row at same index"INV-1042 closed by another user"
The explanation names the row that left.

Step-by-step implementation

Permalink to "Step-by-step implementation"
  1. Capture context before rendering. Record whether focus is in the table, the focused row’s ID and its index.
  2. Re-render. Let the framework update rows; keep stable keys so surviving rows keep their DOM nodes.
  3. Check survival first. If the row still exists, restore focus to it (it may have moved).
  4. Apply the priority order. Same index, previous row, then the results heading.
  5. Explain. Name the departed row and the reason when known; include the result count.

Integration context

Permalink to "Integration context"

Deletion focus is covered in preserving focus when rows are deleted. Filter clearing is in clearing filters without losing keyboard focus. Live data announcements are in aria-live regions for dynamic data, and roving tabindex in implementing roving tabindex for custom data grids.

Gotchas

Permalink to "Gotchas"

Framework re-mounts. Keyed lists that change keys re-create DOM nodes and lose focus even when the row survives; use stable IDs as keys.

Roving tabindex. In grids, update which cell holds tabindex="0" when rows disappear, or the grid becomes unreachable by Tab.

Virtualization. The target row may not be rendered; scroll it into view before focusing.

Edge cases

Permalink to "Edge cases"
  • Bulk removals. If many rows vanish at once, summarise (“12 rows removed”) rather than naming each.
  • Rapid updates. Debounce live updates while the user is mid-navigation to avoid focus jumping under their keys.
  • Undo. If the removal was the user’s action, offer undo and restore focus to the restored row.

Worked example

Permalink to "Worked example"

A ticket queue updated live and removed tickets when they were claimed. Agents navigating with a keyboard found focus jumping to the page top whenever a colleague claimed the ticket they were reading. The team captured focus context before each update, moved focus to the row at the same position, announced “Ticket 4821 claimed by another agent” and paused live removals for two seconds after each key press. Keyboard-only agents stopped losing their place.

Common mistakes

Permalink to "Common mistakes"
  • Doing nothing, so focus drops to body.
  • Moving focus into the table while the user types in a filter.
  • Unstable keys that re-create surviving rows.
  • No explanation of what disappeared.

Testing checklist

Permalink to "Testing checklist"

Testing it with a screen reader

Permalink to "Testing it with a screen reader"

With NVDA, focus a row in the middle of a filtered table. From another window or a test hook, change the filter so that row no longer matches. Listen for the explanation and confirm with Insert+Tab where focus is. Repeat with a filter that empties the table. Then type in the filter box and confirm that focus stays there while the count is announced. In VoiceOver, repeat and confirm the VoiceOver cursor follows focus to the new row.

Communicating the rule to users

Permalink to "Communicating the rule to users"

A predictable rule only helps if users can learn it. Keep the same behaviour across every table in the product, mention it in keyboard help (“If the row you are on disappears, focus moves to the next row”), and use the same message wording each time. Consistency matters more than which target you pick; users of screen readers build mental models quickly, and an exception in one table breaks them for all.

Writing the explanation message

Permalink to "Writing the explanation message"

Keep the message short and lead with the row identifier, because that is what the user was looking at: “INV-1042 no longer matches the filter.” Add the reason when the app knows it, such as “closed by another user” or “moved to Archived”. Finish with the count only if it changed in a way that matters. Avoid technical phrasing like “row removed from view model”. Use a polite region so the message does not interrupt the announcement of the newly focused row, and make sure the newly focused row is announced after it.

Design system notes

Permalink to "Design system notes"

Put focus recovery in the shared table and grid components, keyed by row ID, so feature teams do not reimplement it per screen.

FAQ

Permalink to "FAQ"
Why not move focus to the first row?

It loses the user’s place; nearby rows keep context.

Should I always announce?

Announce when the focused row disappears; otherwise the count is enough.

What if the user is in a cell editor?

Warn before removing a row being edited, or defer the update.

Where should focus go when results are empty?

To the results heading or the empty-state message with its actions.

Does this apply to cards and lists?

Yes, to any collection with focusable items.

How do I test it automatically?

Assert document.activeElement after filter changes in component tests.

Permalink to "Related"

← Back to Focus Management in Single-Page Apps