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.
People
77 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.
| Prop | Type | Default | Description |
|---|---|---|---|
| items | ListDetailItem[] | 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 | null | The chosen item's id, or null for none, controlled or initial. |
| onValueChange | (id: string | null) => void | — | Fires from the press, key or Back that changed the choice. |
| onAction | (actionId: string, itemId: string) => void | — | An action button in the detail was pressed. |
| onLayoutChange | (layout: "split" | "stack") => void | — | 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 | "People" | The list's name: its heading, the listbox's accessible name and the Back button's text. |
| morph | "glide" | "arc" | "fade" | "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 | 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" | "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 | 280 | The list's width side by side, in px (at least 200); a rail is always 72px. |
| height | number | string | 480 | The component's height, in px or any CSS length; the list and the detail scroll inside it. |
| emptyLabel | string | "Choose someone to see their details." | What the detail pane says side by side with nothing chosen. |
| renderDetail | (item: ListDetailItem) => React.ReactNode | — | Replaces the block body with your own detail; the header still morphs. |
| size | "sm" | "md" | "lg" | "md" | Row height 48, 56 or 64px. |
| accent | string | "var(--accent-bright)" | Any CSS colour for the selection rule and the chosen row's wash. |
| sound | boolean | 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 | false | Ignores choices and dims the component. |
| className | string | — | Classes for the root element. |