Focus Handoff in Master-Detail Layouts

Permalink to "Focus Handoff in Master-Detail Layouts"

Master-detail layouts — an email client’s message list and reading pane, a CRM’s lead list and lead record, an admin table with a side inspector — put two regions side by side. The list chooses a record; the detail shows it. The question every implementation has to answer is what happens to keyboard focus when the user chooses a record. Leave focus in the list and screen reader users never hear the detail. Move focus into the detail every time and users browsing the list are yanked away on each arrow press.

This guide gives the rules. It belongs to master-detail & split views.

Spec reference

Permalink to "Spec reference"
  • SC 2.4.3 Focus Order (A) — focus must move in a way that preserves meaning and operability; moving focus to the detail on explicit activation does, moving it on every arrow press does not.
  • SC 3.2.1 On Focus (A) — receiving focus must not trigger a change of context. Updating a detail pane as the list’s selection follows focus is acceptable (content change); moving focus out of the list is a change of context.
  • SC 3.2.2 On Input (A) — similar for selection changes made with input.
  • SC 2.4.7 Focus Visible (AA) — the row the user came from should remain visibly selected while they read the detail.
  • SC 4.1.3 Status Messages (AA) — if the detail updates without focus moving, consider a brief status message.
Selection versus activation Sequence between list, detail pane and status: arrow keys change selection and update the detail without moving focus; Enter activates and moves focus to the detail heading; Escape or a Back key returns focus to the same list row. Selection versus activationListDetail paneStatusDown arrow: selection changes; detailupdatesoptional: "Showing Lund & Co"Enter: focus moves to detail headingEscape: focus back to the same row
Arrows browse; Enter commits to reading; Escape comes back.

When to use — and when not to

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

Move focus to the detail pane when the user activates a record — Enter, double-click, or an explicit “Open” button. That signals intent to read or edit the record. Keep focus in the list when the user is only selecting — arrow keys, single click in a list with follow-focus selection — so they can keep browsing.

Do not move focus to the detail on page load just because the first record is preselected. Users arriving at the page expect to start at the top.

The misapplication: “selection follows focus” in the list combined with “focus follows selection” into the detail. Every arrow press moves focus out of the list, making it impossible to browse. A close second is the opposite: Enter opens the record but focus stays on the list row, so a screen reader user hears nothing and may not realise anything opened.

Focus moved on every selection versus only on activation Comparison: moving focus to the detail on every arrow press makes browsing impossible; moving only on Enter preserves browsing and still delivers the detail when asked. Focus moved on every selection versus only on activation✗ Focus moves on every arrowCannot browse the listDetail heading read on every pressUsers lose their placeViolates 3.2.1 On Focus✓ Focus moves only on EnterArrows browse freelyDetail read once, when askedList position keptChange of context only on explicit input

Annotated code example

Permalink to "Annotated code example"
// List: arrows select; Enter activates (ARIA listbox or grid in the list)
list.addEventListener('keydown', (e) => {
  if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
    e.preventDefault();
    const row = moveSelection(e.key === 'ArrowDown' ? 1 : -1);   // roving focus within list
    showDetail(row.dataset.id, { focus: false });                // content change only (SC 3.2.1)
  }
  if (e.key === 'Enter') {
    e.preventDefault();
    lastRowId = activeRow().dataset.id;
    showDetail(lastRowId, { focus: true });                      // explicit activation (SC 2.4.3)
  }
});

function showDetail(id, { focus }) {
  renderDetail(id);
  if (focus) document.getElementById('detail-heading').focus();  // h2 with tabindex="-1"
}

// Detail → list: Escape returns to the row the user came from
detail.addEventListener('keydown', (e) => {
  if (e.key === 'Escape' && !e.defaultPrevented) {
    list.querySelector(`[data-id="${lastRowId}"]`)?.focus();
  }
});
<section aria-labelledby="detail-heading" class="detail-pane">
  <!-- Focus target: the heading names what was opened -->
  <h2 id="detail-heading" tabindex="-1">Lund &amp; Co — Invoice INV-1042</h2>
  …
</section>

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Key Context Expected announcement AT deviations
Down arrow List Next row; detail updates silently —
Enter List “Lund & Co — Invoice INV-1042, heading level 2” JAWS may continue reading the pane
Escape Detail Row in list: “INV-1042, Lund & Co, selected” Escape inside an open combobox closes it first
Tab past the end of detail Detail Next region or page footer —
Deciding where focus goes Flow: did the user activate (Enter or Open) or only select (arrow or click)? Activation moves focus to the detail heading; selection keeps focus in the list; returning uses Escape or a Back button. Deciding where focus goesUser actionEnter, double-click or Open button?activationActivationfocus to detail headingheading names the recordSelection onlykeep focus in list; update detailoptional status messageReturnEscape or Back to listsame row refocused

Integration context

Permalink to "Integration context"

The list half of this pattern is usually a listbox or grid; its focus mechanism is roving tabindex. When the detail updates without focus, see announcing detail panel updates. Moving between the panes with F6 is covered in keyboard navigation between list and detail panes.

Gotchas

Permalink to "Gotchas"

Escape conflicts. Escape also closes menus, comboboxes and dialogs inside the detail. Only treat Escape as “back to list” if nothing inside handled it — check e.defaultPrevented or listen in the bubbling phase.

List re-render while reading. If the list refreshes while focus is in the detail, the row the user came from may be rebuilt. Store its ID, not the element, and look it up when returning.

Narrow screens. On small viewports the panes stack or the detail replaces the list. Then activation is navigation — focus the detail heading as on a new page, and Back returns to the list with the row focused.

Framework notes

Permalink to "Framework notes"

In React, keep lastRowId in state and use a ref callback map to focus rows on return; avoid autoFocus on the detail heading, which fires on every render. In Angular, cdkFocusInitial is for dialogs, not panes — call focus() on a ViewChild heading after the detail renders (afterNextRender). In Vue, nextTick then focus the heading ref.

useEffect(() => {
  if (openedBy === 'activation') headingRef.current?.focus();   // only on explicit activation
}, [recordId, openedBy]);

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Provide a MasterDetail layout component with an explicit onActivate versus onSelect API, a focus target slot for the detail heading and a built-in return key. Encoding the distinction in the API stops teams from wiring focus to selection by accident.

FAQ

Permalink to "FAQ"
Should a single click in the list move focus to the detail?

Usually not. A click selects; double-click or an explicit Open button activates. Mouse users do not lose anything, and keyboard and screen reader users get a consistent model.

Why focus the heading rather than the first field?

The heading tells users what opened. Focusing the first field skips that context and, for forms, can put users straight into edit mode.

What if the detail pane is a dialog?

Then the dialog pattern applies: focus moves into it, Tab is contained, Escape closes it and focus returns to the row. See record inspector panels for the trade-off.

Should Escape be the only way back?

No. Also provide a visible “Back to list” button for pointer users and users who do not know the shortcut, and support F6 to move between panes.

Permalink to "Related"

← Back to Master-Detail & Split Views