Skip to content
txbrendan/ui

List Row

beta

A 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

Upcoming
Sat3 OctDesign reviewRoom 1 · Starts 15:00In person
Sat10 OctDemo day, final roundOnline · Starts 14:30Online
Sat17 OctIncident · Sign-in errors7:50 · 12 users hit

Press a row

status, hoverActions, peek

GoingDesigner · DesignPaid
Josh Tait No replyEngineer · EngineeringUnpaid
Order closes Fri 2 Oct

aside, peek as a node

Cal NashEngineer · not invited
Thu 1 Oct · 19:00 · Room 1
kate.clarke.customer-success-lead@example.comAn element title stays on one line and truncates

onSelect, selectOn, selecting

Photo toggles
Tom HaleSelecting never hides a face
Josh TaitSelecting never hides a face
Mia OkaforSelecting never hides a face
Row toggles
Tom Hale
Josh Tait
Mia Okafor

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. 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"

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={() => {}} />