List Row
betaA row inside a card: a date or a photo or icon, the title and its status, a meta line, and a trailing chip, with hover actions over its end.
Preview
date, icon, trailing, onClick
Press a row
status, hoverActions, peek
aside, peek as a node
onSelect, selectOn, selecting
Install
1. Add the registry to
components.json, once per app. Without it, the shadcn CLI looks up@txbrendanin a public list that txbrendan/ui does not control.npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.
npx shadcn@latest add @txbrendan/theme3. Add the component.
npx shadcn@latest add @txbrendan/list-row
Use it for
- Lists inside a Card: events, people, reminders, files. Wrap the rows in one <div>, so the card's gap does not split them.
- Rows are siblings. Each draws an inset hairline under itself, and the last one in its parent draws none.
- status puts a StatusText right after the title, with its fix: { label: 'Declined', tone: 'danger', fix: { label: 'Try again', onClick } }.
- hoverActions spring in over the row's end while it is hovered, focused, or focused from outside. The trailing content fades under them, and their keys work only then.
- peek opens a peek on the title: PeekCard props for people behind a count, fixes for a problem, or facts. A node, like PeekSpread, gets 480px.
- aside keeps something in view beside the actions, like a Suggestion chip. The row keeps room for the actions, so nothing moves.
- onClick makes the whole row one control named by its title. Controls inside it keep their own presses.
- onSelect makes it selectable. selectOn photo toggles on the photo, and row toggles on the whole row, with Space, Enter, and X.
- Keep leadSize the same down a list, so every title starts at the same x.
Avoid it for
- A row inside a table. Use DataTable.
- A pencil or an edit button for the title. Make the value the target with InlineEdit.
- Hover actions as the only way to do something. Put the main action in the row's press or its page too.
- Rows in <li> wrappers. Each would be the last in its parent, so no hairlines would show.
Props
- title
Required. Words wrap and never break mid-word. An element, like an email address, stays on one line and truncates.
ReactNode
- meta
A muted line under the title.
ReactNode
- date
A date column at the start, like "Sat 3 Oct". The first word sits small above the rest.
string
- icon
An icon in the lead slot, in iconTone.
ReactNode
- iconTone
The icon's color role. neutral is muted.
"neutral" | "brand" | "danger" | "success" | "warning" · default "neutral"
- leading
A photo, like an Avatar, or anything else in the lead slot, in place of icon.
ReactNode
- leadSize
The square lead slot: 21, 28, or 36px, the Avatar sizes.
"sm" | "md" | "lg" · default "md"
- trailing
A chip, count, or button at the end. It fades while the hover actions show.
ReactNode
- status
The row's status, right after the title, drawn by StatusText.
{ label: string; tone?; icon?: ReactNode; fix?: StatusFix; detail? }
- divider
The inset hairline under the row. The last row in its parent never draws one.
boolean · default true
- onClick
Makes the whole row one control, named by its title.
() => void
- hoverActions
Quick actions over the row's end while it is lit. Each action's shortcut works then too.
HoverAction[]
- peek
The title's peek, on hover, focus, or tap.
PeekCardProps | ReactNode
- peekWidth
The peek's width in pixels. Default 280, 380 with people, 320 with fixes, and 480 for a node.
number
- index
The row's place in its list. It fades up in turn as it first renders.
number
- onSelect
Makes the row selectable.
() => void
- selected
A square check on the photo's corner, and a 2px ring around it. It never hides the face.
boolean · default false
- selectOn
photo: the photo toggles, and the rest of the row runs onClick. row: the whole row toggles, and onClick is not used.
"photo" | "row" · default "photo"
- selecting
Anything in the list is selected: checks stay in view, a row press toggles, and the hover actions step aside for a BulkBar.
boolean · default false
- focused
Lit from outside, as a list that moves its own highlight does. Shows the hover actions and turns their keys on.
boolean · default false
- aside
Stays in view beside the hover actions, like a Suggestion chip.
ReactNode
- className
Extra classes on the row. Later classes win.
string
- size
SelectBadge only, the check for custom rows: sm sits on a 21px photo, md on 28px, lg on 36px.
"sm" | "md" | "lg" · default "md"
| Prop | Type | Default | What it does |
|---|---|---|---|
| title | ReactNode | - | Required. Words wrap and never break mid-word. An element, like an email address, stays on one line and truncates. |
| meta | ReactNode | - | A muted line under the title. |
| date | string | - | A date column at the start, like "Sat 3 Oct". The first word sits small above the rest. |
| icon | ReactNode | - | An icon in the lead slot, in iconTone. |
| iconTone | "neutral" | "brand" | "danger" | "success" | "warning" | "neutral" | The icon's color role. neutral is muted. |
| leading | ReactNode | - | A photo, like an Avatar, or anything else in the lead slot, in place of icon. |
| leadSize | "sm" | "md" | "lg" | "md" | The square lead slot: 21, 28, or 36px, the Avatar sizes. |
| trailing | ReactNode | - | A chip, count, or button at the end. It fades while the hover actions show. |
| status | { label: string; tone?; icon?: ReactNode; fix?: StatusFix; detail? } | - | The row's status, right after the title, drawn by StatusText. |
| divider | boolean | true | The inset hairline under the row. The last row in its parent never draws one. |
| onClick | () => void | - | Makes the whole row one control, named by its title. |
| hoverActions | HoverAction[] | - | Quick actions over the row's end while it is lit. Each action's shortcut works then too. |
| peek | PeekCardProps | ReactNode | - | The title's peek, on hover, focus, or tap. |
| peekWidth | number | - | The peek's width in pixels. Default 280, 380 with people, 320 with fixes, and 480 for a node. |
| index | number | - | The row's place in its list. It fades up in turn as it first renders. |
| onSelect | () => void | - | Makes the row selectable. |
| selected | boolean | false | A square check on the photo's corner, and a 2px ring around it. It never hides the face. |
| selectOn | "photo" | "row" | "photo" | photo: the photo toggles, and the rest of the row runs onClick. row: the whole row toggles, and onClick is not used. |
| selecting | boolean | false | Anything in the list is selected: checks stay in view, a row press toggles, and the hover actions step aside for a BulkBar. |
| focused | boolean | false | Lit from outside, as a list that moves its own highlight does. Shows the hover actions and turns their keys on. |
| aside | ReactNode | - | Stays in view beside the hover actions, like a Suggestion chip. |
| className | string | - | Extra classes on the row. Later classes win. |
| size | "sm" | "md" | "lg" | "md" | SelectBadge only, the check for custom rows: sm sits on a 21px photo, md on 28px, lg on 36px. |
Accessibility
- With onClick, the row's press is one real button stretched over the row, named by the title and described by the meta line. The focus ring goes around the whole row.
- Controls inside the row, like the status fix, the peek, the trailing button, and the hover actions, are their own Tab stops and keep their own presses. No control sits inside another.
- selectOn photo makes the photo a checkbox named "Select" and the title. selectOn row makes the row's press a checkbox, and X toggles it too.
- The selected ring is an outline, and the check has a border, so both stay in forced colors.
- Hover actions show on hover and on focus inside the row. Their single-key shortcuts work only while the row is lit.
- The row carries data-peek-row, so a peek opens beside it, and ↑↓ step between the peeks of a list.
Examples
Dated rows with an icon
import { CalendarDays } from "lucide-react";
<div className="rounded-xl border bg-card py-3">
<ListRow date="Sat 3 Oct" icon={<CalendarDays />} iconTone="brand" title="Design review" meta="Room 1 · 15:00" />
<ListRow date="Sat 10 Oct" icon={<CalendarDays />} iconTone="brand" title="Demo day" meta="Online · 14:30" />
</div>A person with a status and quick actions
import { MessageSquare, UserRound } from "lucide-react";
<ListRow
icon={<UserRound />}
title="Josh Tait"
meta="Engineer"
status={{ label: "No reply", tone: "warning", fix: { label: "Remind Josh", onClick: () => {} } }}
hoverActions={[{ icon: <MessageSquare />, label: "Message", shortcut: "M" }]}
/>Selectable
import { UserRound } from "lucide-react";
<ListRow icon={<UserRound />} title="Tom Hale" selectOn="row" selected onSelect={() => {}} />