Master Detail
A two-panel layout where a list of items (master) drives a contextual detail view on the right. Collapses to a single-panel push navigation on narrow screens.
Preview
Team · 5 members
On narrow screens, tap any row to push to the detail view. Hit Back to return to the list.
When to use
Master–detail works best when you have a collection of similar items and rich per-item content that would waste space if shown for every row.
Good fits
- →Email / messaging inboxes
- →CRM contact or lead views
- →Settings pages with categories
- →File browsers and asset managers
- →Issue trackers and task lists
Poor fits
- →Simple lists where all data fits in one view
- →Workflows requiring both panels simultaneously
- →Content where comparison across items matters
- →Very shallow item counts (< 5 items)
Layout anatomy
The pattern composes three zones. Each has a clear responsibility.
| Zone | Width | Responsibility |
|---|---|---|
Master (list) | 240–320px fixed | Scannable rows. One item is always selected. Owns the selection state. |
Divider | 1px | Visual boundary. Can be a drag handle for resizable layouts. |
Detail | flex: 1 (remaining) | Rich content for the selected item. Scrolls independently of the list. |
Responsive behavior
On narrow screens the two panels cannot coexist. The correct pattern is a push navigation — selecting an item slides the detail in from the right, replacing the list. A back button returns to the list.
| Breakpoint | Layout | Behavior |
|---|---|---|
≥ 768px (md) | Side by side | Both panels visible. Selecting a row updates the detail in place. |
< 768px | Single panel | Only one panel visible at a time. Row tap pushes detail. Back button returns to list. |
Implementation
The React implementation uses a single showDetail boolean to toggle panel visibility on mobile. On desktop, both panels are always rendered and visible — no JS is needed to switch between them, just md:flex and hidden utility classes. The SwiftUI implementation uses NavigationSplitView which handles the column/stack transition automatically.
"use client";
import { useState } from "react";
interface Item { id: number; title: string; /* … */ }
export function MasterDetail({ items }: { items: Item[] }) {
const [selected, setSelected] = useState<Item>(items[0]);
const [showDetail, setShowDetail] = useState(false);
return (
<div className="flex h-full border rounded-xl overflow-hidden">
{/* Master list — hidden on mobile when detail is open */}
<div className={cn(
"w-[260px] flex-shrink-0 border-r flex flex-col",
showDetail && "hidden md:flex"
)}>
<div className="px-4 py-3 border-b">
<p className="text-[11px] font-semibold uppercase tracking-wider text-muted">
{items.length} items
</p>
</div>
<div className="flex-1 overflow-y-auto">
{items.map((item) => (
<button
key={item.id}
onClick={() => { setSelected(item); setShowDetail(true); }}
className={cn(
"w-full px-4 py-3 text-left border-b last:border-0 transition-colors",
selected.id === item.id
? "bg-accent-subtle text-accent"
: "hover:bg-surface"
)}
>
{item.title}
</button>
))}
</div>
</div>
{/* Detail — hidden on mobile when list is shown */}
<div className={cn("flex-1 flex flex-col", !showDetail && "hidden md:flex")}>
{/* Back button — mobile only */}
<button
onClick={() => setShowDetail(false)}
className="md:hidden px-4 py-3 border-b text-left text-accent"
>
← Back
</button>
<div className="flex-1 overflow-y-auto p-6">
{/* Render selected item detail */}
<h1>{selected.title}</h1>
</div>
</div>
</div>
);
}Design decisions
- →Always have a selection. On load, default-select the first item so the detail panel is never empty. An empty detail state creates a jarring visual hole.
- →Keep the master list narrow. 240–320px is the sweet spot. Wider and the detail panel loses valuable real estate. Narrower and row content gets truncated too aggressively.
- →Selected row highlight. Use a subtle accent background (not a strong color) for the selected row. The detail panel already makes the selection obvious — the list highlight is just a secondary cue.
- →Independent scroll areas. The master list and the detail both scroll independently. The outer container has overflow: hidden; each panel has overflow-y: auto with flex: 1.
- →Back button on mobile. Use native browser history (router.push / NavigationLink) when possible so the hardware back button and gesture work without extra code.
Accessibility
- →The master list should have role="listbox" and each item role="option" with aria-selected.
- →When a new item is selected, move focus to the detail heading so keyboard and screen reader users know the panel has updated.
- →The back button on mobile must be visible and focusable — don't hide it with opacity or pointer-events-none.
- →Both panels should be keyboard reachable via Tab. On desktop, Tab should cycle: list → detail → list.
- →On mobile, when the detail slides in, trap focus within it until the user presses Back.