KQ-1285 · LIST/DETAIL

List Detail

A master list whose chosen row becomes the page: the row's avatar, title, subtitle and meta are measured and fly to their places in the detail header, scaling from the row's size to the header's, while the body streams in below a block at a time on springs.glide. Stacked, the row's own surface grows from its rectangle to fill the box on glide as the list fades beneath it, and Back runs the morph in reverse until the parts land on their row; side by side, the parts fly across between the panes while the previous header flies home as a ghost and a selection rule slides down the list on springs.snap, and with morph="arc" each part's x runs on snap and its y on glide so it travels a curve. The list is a listbox with one tab stop (arrows and Home or End move, Enter or Space choose), and stacked, focus moves to the detail's heading and Escape or Back returns it to the row.

KQ-1285 · LIST/DETAIL

People

7
  • Ines OkaforDesign leadLisbon
  • Tomas ReyesPlatform engineerMexico City
  • Amara LindqvistProduct managerStockholm
  • Kenji MoriData scientistOsakaAway
  • Priya NatarajanHead of supportPune
  • Lucas FerreiraSecurity engineerPorto
  • Hana KovacFinance partnerZagreb

7 people · stacked · choose someone

Install

One command — the source lands in your repo. Or copy it from the Code tab.

$ pnpm dlx shadcn@latest add @kinetiq/list-detail

Props

items
ListDetailItem[]
default defaultListDetailItems
The list in order: id, title, subtitle, meta, avatar (initials, tint, icon), badge and the detail's blocks (text, fields, list, actions). defaultListDetailItems is the seven-person Fernworks team.
value / defaultValue
string | null
default null
The chosen item's id, or null for none, controlled or initial.
onValueChange
(id: string | null) => void
default —
Fires from the press, key or Back that changed the choice.
onAction
(actionId: string, itemId: string) => void
default —
An action button in the detail was pressed.
onLayoutChange
(layout: "split" | "stack") => void
default —
Fires when the layout moves between stacked and side by side, whether a resize or the split prop moved it, and once when first measured.
label
string
default "People"
The list's name: its heading, the listbox's accessible name and the Back button's text.
morph
"glide" | "arc" | "fade"
default "arc"
How the row's parts travel to the header: straight on glide, on a curve (x on snap, y on glide, cascaded), or not at all (a fade).
stream
number
default 50
The wait between one body block arriving and the next, in ms; the total is held under 600ms, and 0 brings the body in at once.
split
"auto" | "stack" | "rail"
default "auto"
How list and detail share the box: side by side from 36rem of container width and stacked below it, always stacked, or always side by side with the list as a narrow rail of avatars (each named and titled).
listWidth
number
default 280
The list's width side by side, in px (at least 200); a rail is always 72px.
height
number | string
default 480
The component's height, in px or any CSS length; the list and the detail scroll inside it.
emptyLabel
string
default "Choose someone to see their details."
What the detail pane says side by side with nothing chosen.
renderDetail
(item: ListDetailItem) => React.ReactNode
default —
Replaces the block body with your own detail; the header still morphs.
size
"sm" | "md" | "lg"
default "md"
Row height 48, 56 or 64px.
accent
string
default "var(--accent-bright)"
Any CSS colour for the selection rule and the chosen row's wash.
sound
boolean
default false
A click when a row is chosen, pitched by its place in the list, and a swish as the page opens or goes back.
disabled
boolean
default false
Ignores choices and dims the component.
className
string
default —
Classes for the root element.