Master-Detail & Split Views
Permalink to "Master-Detail & Split Views"Master-detail is the layout of almost every serious data application: a list or table of records on one side, the selected record’s details on the other. Mail clients, CRMs, issue trackers, admin consoles, file managers and log viewers all use it. It is efficient for sighted mouse users because both halves are visible and one click links them. For keyboard and screen reader users, the same layout raises a series of questions the visual design never has to answer: when does focus move from the list to the detail? What do users hear when the detail changes but focus does not? How do they get from one pane to the other without tabbing through everything in between? Is the slide-in inspector a dialog or a region? And how do they resize the panes when the table needs more room?
The users most affected are screen reader users, who cannot see the detail update and need a model of where focus is; keyboard users, who need fast movement between panes; magnifier users, who often need one pane collapsed so the other fits at their zoom level; and users with cognitive disabilities, who are disoriented when focus jumps unexpectedly. The failure mode this topic prevents is a split view that is fast with a mouse and a maze without one.
This topic belongs to accessible data tables & grid systems.
WCAG criteria in scope
Permalink to "WCAG criteria in scope"| Criterion | Level | Relevance to master-detail layouts |
|---|---|---|
| 1.3.1 Info and Relationships | A | Panes as named regions; detail summaries tied to rows |
| 2.1.1 Keyboard | A | Activation, pane movement and splitter all keyboard-operable |
| 2.1.2 No Keyboard Trap | A | Non-modal panels never trap focus |
| 2.4.1 Bypass Blocks | A | F6, skip links and landmarks bypass the long list |
| 2.4.3 Focus Order | A | Focus moves on activation and returns to the same row |
| 2.5.7 Dragging Movements | AA | Splitter has a non-drag alternative |
| 3.2.1 On Focus | A | Moving through the list does not move focus elsewhere |
| 4.1.3 Status Messages | AA | Optional summaries and collapse messages |
Prerequisites
Permalink to "Prerequisites"- A list built as a listbox or grid with roving tabindex.
- Focus management in single-page apps, since details often load asynchronously and lists re-render.
- Keyboard focus trapping & navigation for inspectors implemented as modal dialogs.
- Accessible names & descriptions for data widgets, used for pane names and row summaries.
ARIA & HTML spec reference
Permalink to "ARIA & HTML spec reference"| Element / attribute | Valid values | When to apply | Common misuse |
|---|---|---|---|
section aria-labelledby |
Heading ID | Each pane | Unnamed div panes |
aside / complementary |
— | Persistent secondary pane | Several unnamed aside elements |
dialog / showModal() |
— | Blocking inspectors | Custom traps on non-blocking drawers |
role="dialog" (non-modal) |
— | Transient, non-blocking inspectors | Adding aria-modal="true" |
h2 tabindex="-1" |
— | Focus target on activation | Focusing the pane container |
aria-describedby on rows |
ID of summary | Key detail fields in the list | Duplicating the row text |
role="separator" + tabindex="0" |
— | Resizable divider | Drag-only divs |
aria-valuenow/min/max/text |
Numbers / text | On the splitter | Pixel position instead of pane size |
Step-by-step implementation
Permalink to "Step-by-step implementation"Step 1 — Selection versus activation (SC 3.2.1)
Permalink to "Step 1 — Selection versus activation (SC 3.2.1)"Arrows change selection and update the detail silently; Enter activates. See focus handoff in master-detail layouts.
if (e.key.startsWith('Arrow')) showDetail(id, { focus: false });
if (e.key === 'Enter') showDetail(id, { focus: true });
Step 2 — Focus targets and return (SC 2.4.3)
Permalink to "Step 2 — Focus targets and return (SC 2.4.3)"Focus the detail heading on activation; Escape returns to the same row by ID.
detailHeading.focus(); /* … */ list.querySelector(`[data-id="${lastId}"]`).focus();
Step 3 — Summaries on demand (SC 1.3.1)
Permalink to "Step 3 — Summaries on demand (SC 1.3.1)"Attach a short summary of key detail fields to each row with aria-describedby, as in announcing detail panel updates.
<li role="option" aria-describedby="sum-17">Lund & Co<span id="sum-17" hidden>Risk high.</span></li>
Step 4 — Choose the detail container (SC 2.1.2)
Permalink to "Step 4 — Choose the detail container (SC 2.1.2)"Modal dialog, non-modal dialog or region — matched to how the panel looks. See record inspector panels: dialog or region?.
editing ? inspector.showModal() : openPeek(id);
Step 5 — Move between panes (SC 2.4.1)
Permalink to "Step 5 — Move between panes (SC 2.4.1)"F6 and Shift+F6, skip links and named regions. See keyboard navigation between list and detail panes.
if (e.key === 'F6') { e.preventDefault(); cycle(e.shiftKey ? -1 : 1); }
Step 6 — Resize with a window splitter (SC 2.1.1, 2.5.7)
Permalink to "Step 6 — Resize with a window splitter (SC 2.1.1, 2.5.7)"See window splitter pattern for resizable panes.
<div role="separator" tabindex="0" aria-label="Invoices list width" aria-valuenow="40" aria-valuemin="20" aria-valuemax="80"></div>
Keyboard interaction contract
Permalink to "Keyboard interaction contract"| Key | Context | Action | Expected AT announcement | Failure indicator |
|---|---|---|---|---|
| Up / Down | List | Select previous / next | Row name (+ summary after pause) | Focus jumps to detail |
| Enter | List | Activate; focus detail heading | Heading naming the record | Nothing announced |
| Escape | Detail | Return to originating row | Row name | Focus to body |
| F6 / Shift+F6 | Anywhere | Next / previous pane | Pane’s last focused element | Tab-only movement |
| Arrows | Splitter | Resize | New value | Silent; no change |
| Enter | Splitter | Collapse / restore | “collapsed” + status | No collapse |
Screen reader compatibility matrix
Permalink to "Screen reader compatibility matrix"| AT + browser | Detail heading on activation | Row descriptions | Splitter value |
|---|---|---|---|
| NVDA + Firefox | Read with level | Read after pause | Announced |
| NVDA + Chrome | Read | Read after pause | Announced |
| JAWS + Chrome | Read; may continue reading pane | Verbosity dependent | Announced |
| VoiceOver + Safari | Read | Read after pause | Sometimes silent on change |
| TalkBack + Chrome | Read | Inconsistent | Rarely announced |
| Narrator + Edge | Read | Read | Announced |
Edge cases & failure modes
Permalink to "Edge cases & failure modes"1. Focus follows selection into the detail
Permalink to "1. Focus follows selection into the detail"Diagnosis: every arrow press moves focus out of the list. Fix: move focus only on explicit activation.
2. Escape swallowed or overloaded
Permalink to "2. Escape swallowed or overloaded"Diagnosis: Escape closes a combobox and also returns to the list in one press. Fix: treat Escape as “back to list” only when nothing inside the pane handled it.
3. Drawer that looks open but traps focus
Permalink to "3. Drawer that looks open but traps focus"Diagnosis: a non-modal panel implemented with a focus trap. Fix: a non-modal dialog without a trap, plus F6.
4. Lost row after list refresh
Permalink to "4. Lost row after list refresh"Diagnosis: the stored row element was replaced. Fix: store the row ID and look it up on return.
5. Magnifier users cannot widen the table
Permalink to "5. Magnifier users cannot widen the table"Diagnosis: drag-only divider. Fix: window splitter with Enter to collapse.
Focus handoff in master-detail layouts
Permalink to "Focus handoff in master-detail layouts"Arrows browse and Enter commits. Focus goes to a heading that names the record, and Escape brings users back to the same row. See focus handoff in master-detail layouts.
<h2 id="detail-heading" tabindex="-1">Lund & Co — INV-1042</h2>
Announcing detail panel updates
Permalink to "Announcing detail panel updates"Decide whether the detail adds what the row lacks; if so, expose a short summary on demand with a description, not a full-pane announcement. See announcing detail panel updates.
setTimeout(() => { status.textContent = summaryFor(id); }, 700); // fallback only
Record inspector panels — dialog or region?
Permalink to "Record inspector panels — dialog or region?"Match semantics to appearance: dimmed background means modal; a panel beside a usable table means non-modal; a permanent pane means region. See record inspector panels: dialog or region?.
<aside role="dialog" aria-labelledby="peek-h">…</aside> <!-- non-modal: no aria-modal -->
Keyboard navigation between list and detail panes
Permalink to "Keyboard navigation between list and detail panes"F6 cycles panes, restoring each pane’s last focus; skip links and named regions serve users who do not know F6. See keyboard navigation between list and detail panes.
<a class="skip-link" href="#detail-h">Skip to record details</a>
Window splitter pattern for resizable panes
Permalink to "Window splitter pattern for resizable panes"A focusable separator with a value, arrow keys and Enter to collapse, plus a non-drag control. See window splitter pattern for resizable panes.
if (e.key === 'Enter') value > 0 ? collapse() : restore();
Cross-cutting concerns
Permalink to "Cross-cutting concerns"Responsive collapse. Below a breakpoint, most split views stack or replace the list with the detail. At that point activation becomes navigation: focus the detail heading as if a new page loaded, provide a Back control, and restore the list row on return. The same rules apply at 400% zoom on desktop, where the layout reflows to the narrow form — test it with zoom and reflow testing at 400 percent.
Routing. If selecting a record changes the URL, decide whether selection or only activation pushes history. Pushing on every arrow press floods history; replacing on selection and pushing on activation keeps Back meaningful.
Asynchronous details. Details often load after selection. Show a loading state in the pane, mark it aria-busy, and do not move focus into a pane that has not rendered its heading yet. If activation happens before the load completes, focus the heading once it exists.
Unsaved changes. Selecting another row while the detail has unsaved edits must not silently discard them. Ask in a modal confirmation, and return focus to the list row the user tried to select if they choose to discard.
Worked example: an invoice triage screen
Permalink to "Worked example: an invoice triage screen"Consider an accounts-payable screen: a list of 300 invoices on the left, the selected invoice on the right with its line items, approval history and an Approve button. Here is how the patterns combine for a screen reader user working through the queue.
They arrive on the page, which announces its heading and the list region. Pressing Down moves through invoices; each row reads supplier, amount and due date, and after a short pause a description adds “Duplicate suspected” for the few rows where that applies. On an interesting row they press Enter: focus moves to the detail heading, “INV-1042, Lund & Co”, and they read the line items with table navigation. They Tab to Approve and activate it; the status region says “Approved. 299 invoices remaining”, and focus returns to the list on the next invoice, because approving removed the current one. When the line-item table is too wide at their zoom level, they Tab to the splitter and press Enter to collapse the list, then press Enter again to restore it.
Nothing in that session required a mouse or a guess about where focus had gone. Every move was the result of a key they chose, and every change that mattered was either where focus landed or in a short status message.
Who is affected, and how
Permalink to "Who is affected, and how"| User group | Typical failure in split views | Pattern that fixes it |
|---|---|---|
| Screen reader users | Detail changes unheard; focus jumps unexpectedly | Activation-only focus moves; summaries |
| Keyboard users | Tabbing through the whole list to reach the detail | F6 and skip links |
| Magnifier users | Wide tables do not fit beside the other pane | Splitter with collapse |
| Users with cognitive disabilities | Focus moves without a clear cause | Predictable, user-initiated moves |
| Switch users | Many steps between panes | F6 and direct pane shortcuts |
Design system integration
Permalink to "Design system integration"A MasterDetail or SplitView component should encode the decisions in this topic:
- An API that distinguishes
onSelectfromonActivate, so focus moves only on activation. - A detail slot that requires a heading and receives focus on activation.
- Named panes, F6 cycling with last-focus memory, and generated skip links.
- A splitter implementing the window splitter pattern, with persistence and collapse.
- Variants —
region,peek(non-modal dialog),modal— chosen by name, each owning its focus and Escape rules. - Responsive behaviour that switches to page-style navigation at a breakpoint, including Back and row restoration.
Tokens are needed for the selected row in the list while focus is in the detail (it must remain visible, at 3:1 against the row background), the splitter grip and its focus ring.
Testing checklist
Permalink to "Testing checklist"Automated
Permalink to "Automated"Keyboard
Permalink to "Keyboard"Screen reader
Permalink to "Screen reader"FAQ
Permalink to "FAQ"Should the detail update as the user arrows through the list?
It can — that is what sighted users expect — as long as focus stays in the list. Screen reader users hear the row and, optionally, a short summary.
Is a split view harder to make accessible than separate pages?
Slightly, because two regions share one page. In return it can be faster for keyboard users than page navigation. The patterns here close the gap.
How do I choose between F6 and skip links?
Use both. F6 is fast and expected by experienced users; skip links are discoverable and work for everyone else.
What is the most common master-detail bug?
Moving focus into the detail on every selection change. It makes the list unusable from the keyboard and violates 3.2.1.
Should the list show which record is open in the detail?
Yes. Keep the opened row visibly selected (with a non-colour cue such as a left border or check icon) and expose it with aria-selected or aria-current, so users returning from the detail know which row they left. This matters most for magnifier users, who may see only the list after collapsing the detail.
How do I test a split view quickly?
Use only the keyboard: arrow through ten rows, open one, Escape back, press F6 twice, and collapse a pane with the splitter. Then repeat with a screen reader running. Any moment where you are unsure where focus is, or where a change was silent, is a bug.
Related
Permalink to "Related"- Expandable rows & nested data — details shown inline instead of beside
- Keyboard focus trapping & navigation — modal inspectors
- Composite widget roles & states — the list’s role
- Accessible data dashboards — multi-region layouts at larger scale
- Responsive data tables — what happens when space runs out