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.

ZoneWidthResponsibility
Master (list)240–320px fixedScannable rows. One item is always selected. Owns the selection state.
Divider1pxVisual boundary. Can be a drag handle for resizable layouts.
Detailflex: 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.

BreakpointLayoutBehavior
≥ 768px (md)Side by sideBoth panels visible. Selecting a row updates the detail in place.
< 768pxSingle panelOnly 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.

MasterDetail.tsx
tsx
"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.