Agent docs

ListItem

One row of a list-in-card: icon or avatar, title, subtitle, metadata, actions.

One row of a list-in-card: a leading icon or avatar, a title and subtitle, muted metadata on the right, and an optional actions zone. Rows are meant to sit flush inside a bordered surface, a Card body or any divide-y container, and pick up a hover background, never an underline, when interactive (list-item.tsx:6-12). href makes the whole row navigate via a stretched-link overlay, while actions stays independently clickable above it.

The canon behind it. docs/design-language/05-cards-and-surfaces.md §A8 (05-cards-and-surfaces.md:38, names ListItem outright as "the row-in-card primitive") and docs/design-language/06-tables.md §A1 (06-tables.md:15, pick the right layer, the same reasoning that draws the ListItem-versus-Table line this doc opens with).

When to reach for it#

A simple, flush, icon/avatar-led row list: contacts, files, a picker of records that are not columnar data. This is a different shape from the table cluster (table.md): one row per record with a fixed icon/title/subtitle/meta/actions layout, not a grid of arbitrary columns. Reach for Table/DataTable instead when the data actually has more than one meaningful column to show side by side.

No shipped usage was found in either app. A tree-wide grep for both the import and the JSX tag across walmart-mvp/frontend/src and speedway turned up nothing live. The only reference anywhere is a commented-out, deprecated block: speedway/app/routes/workspaces/select.tsx:39-46,176-203 imports and renders ListItem for the old workspace-selector list, but the whole route was replaced by a redirect on 2026-07-21 (doc-29, the workspace concept is hidden from users) and the original UI, ListItem included, was kept commented out verbatim so it "can be revived wholesale if workspaces come back" (select.tsx:4-9). Treat this as a dormant, not a live, precedent: it shows the component's intended shape, but nothing in either app currently renders a ListItem.

Contract#

  • Icon / iconSize (default 16) / iconProps / iconClassName (IconImportProps): the leading glyph. avatar: a node that wins over Icon in the same leading disc.
  • title (required) / subtitle: through renderNode, string or JSX. title renders as a link (<a> or linkAs) when href is set, plain text otherwise.
  • meta: right-aligned muted text (counts, timestamps, a role).
  • actions: a right-side interactive zone. Stays clickable even when the row itself is a link: the link overlay is a stretched after:absolute after:inset-0 pseudo-element on the title (list-item.tsx:83), and actions renders in its own relative wrapper so it stacks above that overlay rather than being swallowed by it (list-item.tsx:97).
  • href / linkAs: makes the whole row navigate. linkAs injects the app router's Link component; defaults to a plain <a>.
  • onClick: row click handler for non-link rows; ignored when href is set.
  • active: a background tint for the current/selected row. disabled: pointer-events-none plus half opacity.
  • skeleton: an avatar circle, two text bars, and a trailing bar.
  • Slot classnames per ListItemClassNames: "" the row, iconWrap, title, subtitle, meta, actions.

The dormant precedent#

The commented-out select.tsx usage (:176-203) is worth reading as the one authored example of the full contract in use, even though it is dead code today: Icon="FileSheet", title={w.name}, subtitle={w.slug}, href/linkAs={RowLink} for row-as-link navigation, a meta node combining two counts and a Timestamp, and actions={pending ? <Spinner size="sm" /> : undefined} for an in-flight navigation indicator that stays visible over the link overlay.

Banned combinations#

None found; there is no shipped misuse because there is no shipped use.

Before you adopt this#

Five questions to answer before reaching for ListItem.

  1. Does the shell, a parent layout, or a global provider already render this? Not applicable, ListItem is page content; no shell already renders it.
  2. Does this app already ship a local implementation of the same thing? Not applicable, no local ListItem clone is documented in either app.
  3. Does this app's kit pin reach the version this component or prop landed in? Not applicable, no shipped call site exists in either app as of this pass.
  4. Does the component derive its own accessible name and keyboard path, or must the call site supply them? The stretched-link title supplies the row's name; actions controls must name themselves.
  5. Which canon §A rules bind this surface, and which does the composition break? §A8 names ListItem the row-in-card primitive, distinct from a table column.

Travels with#

  • Card, as the flush-row container, per the source comment.
  • EmptyState, for a list-in-card's empty state. Unconfirmed: no shipped pairing found (the dormant select.tsx precedent does pair the two, EmptyState for the zero-workspace case beside ListItem for the populated case, select.tsx:152-167).
  • NavLinkComponent (app-shell.types.ts), the same router-link type linkAs shares with AppShell's nav.

Snippet#

// speedway/app/routes/workspaces/select.tsx:176-203
// Commented out, doc-29; shown for the shape, not as a live call site.
<ListItem
key={w.id}
Icon="FileSheet"
title={w.name}
subtitle={w.slug}
href={target}
linkAs={RowLink}
meta={
<span className="flex items-baseline gap-3">
<span>
<b className="text-base-content font-semibold">{w.fileCount ?? 0}</b>{" "}
{w.fileCount === 1 ? "file" : "files"}
</span>
<span>
<b className="text-base-content font-semibold">{num(w.partCount ?? 0)}</b>{" "}
SKUs
</span>
<span className="text-base-content/65">
<Timestamp iso={w.updatedAt} />
</span>
</span>
}
actions={pending ? <Spinner size="sm" /> : undefined}
/>
@versable-git/ui · reference, canon, and method, read in place