Record List
The List view of a table. Each record on the current page is one row: a title, up to four labelled values, choice badges, and row actions. Use it to read and triage records. Use Table to compare values in columns, edit, or select rows in bulk.
- TF-101Seed tray leaks onto the potting bench
- Assignee
- Ana Ruiz
- Due
- Oct 01
- Points
- 1
- Requester
- Robin Hale
OpenUrgent - TF-102Order compost for the north beds
- Assignee
- Chen Wei
- Due
- Oct 08
- Points
- 6
- Requester
- Robin Hale
In progressLow - TF-103Fix the greenhouse vent motor that stops halfway open on cold mornings after the frost alert runs
- Due
- Oct 15
- Points
- 11
- Requester
- Greenhouse operations team, north site, weekend shift
- Site
- Packing shed
BlockedHigh - TF-104Frost alert texts arrive late
- Assignee
- Ben Okafor
- Points
- 3
- Requester
- Robin Hale
- Site
- East field
Done - TF-105Label printer jams on seed packets
- Assignee
- Dara Singh
- Due
- Oct 01
- Requester
- Robin Hale
- Site
- North greenhouse
OpenNormal - TF-106Update the spring market price list
- Assignee
- Ana Ruiz
- Due
- Oct 08
- Points
- 13
- Requester
- Robin Hale
In progressUrgent
Click a row, or focus the list and press Enter, to edit it.
Usage
import { Badge } from "@tulipfarm/sprout/badge";
import { RecordList, type RecordListHandle } from "@tulipfarm/sprout/record-list";
const list = useRef<RecordListHandle>(null);
<RecordList
ref={list}
label="Tickets"
items={page.rows}
status={read.loading ? "loading" : read.failed ? "error" : "ready"}
onRetry={read.retry}
values={(ticket) => [
{ label: "Assignee", value: ticket.assignee },
{ label: "Due", value: ticket.due },
]}
badges={(ticket) => <Badge variant="info">{ticket.status}</Badge>}
renderActions={(ticket) => <TicketActions ticket={ticket} />}
onItemOpen={(ticket) => openSheet(ticket)}
footer={<TicketPagination onPageChange={() => list.current?.focusFirst()} />}
/>items:{ id, title }[]plus your own fields, in page order.titleis plain text. Screen readers use it, and it shows in full on hover.labelnames the list. The row count is added, so screen readers hear “Tickets, 6 rows”.renderTitledraws the title, such as a key before the text. Screen readers still usetitle.valuesreturns{ label, value, text? }[]. Only the first four show; the rest are left out. Leave out empty values before you return them. Usetextfor the hover hint whenvalueis not a string or number.badgesreturns choice badges, such as status and priority. Leave out empty choices.renderActionsis a free slot for row actions, such as an Edit icon button and aDropdownMenu. Clicks in it never open the row. Give each action a name that includes the row.onItemOpenruns on a click anywhere else in the row, or on Enter. A click that ends a text selection does not open the row.footersits under the rows inside the panel. Put yourPaginationthere.density="compact"uses 8px by 12px padding instead of 12px by 16px.
States
status is "ready", "loading", or "error". While a page loads and rows are already shown, they stay in place, dimmed, with aria-busy and a “Loading rows…” announcement. With no rows yet, five skeleton rows hold the space.
"error" replaces the rows with an error state. error is its description, andonRetry adds a Try again button. When items is empty and ready, the list showsempty: pass a StatePanel or text. Without it, the list says “No rows to show”.
<RecordList
label="Tickets"
items={[]}
empty={
<StatePanel kind="empty" heading="h3" title="No rows match" description="No tickets match the current filters.">
<Button variant="outline" onClick={clearFilters}>Clear filters</Button>
</StatePanel>
}
/>Layout
Rows sit in one bordered panel, separated by hairlines. The title is on the first line in 14px medium text. Values follow on the second line as muted label and strong value pairs, with tabular numerals, 16px apart, wrapping as needed. Badges and actions sit on the right.
Long titles cut off after one line. Each value cuts off at 16rem. The full text shows on hover and is always read by screen readers. When the list is 640px wide or less, badges move under the values, actions stay at the top right, and titles wrap to two lines.
Keyboard and focus
- The list is one tab stop. Tab lands on the last focused row, or the first row.
- Up and Down move between rows. Home and End go to the first and last row. Enter opens the row.
- Tab from a row reaches that row's actions, then leaves the list. Actions in other rows are out of the tab order until their row is focused. Clicking an action also makes its row the focused row.
- Menus opened from actions work as usual, and focus returns to their trigger when they close. Shift+Tab from an action goes back to its row.
After a page change, call focusFirst() on the ref so focus does not fall to the page. Whilestatus is "loading", it waits for the new rows, so you can call it from the page button's click handler. With no rows, it focuses the empty or error state.
Accessibility
The list is a named list, and each row is a listitem named by its title. The values and badges describe the row, so screen readers read them after the title. Values are a description list of field and value. Rows have a hover color and an inset focus ring. Rows fade in over 180ms and the dim changes smoothly only when reduced motion is not requested. In forced colors mode the lines use the system text color.
Theme tokens
The panel uses --sprout-surface, --sprout-line, and --sprout-radius-panel. Titles and values use --sprout-text, labels use --sprout-muted, and hover uses--sprout-hover. Focus uses --sprout-focus-ring and --sprout-focus-width. Motion uses --sprout-motion-fast, --sprout-motion-normal, and--sprout-motion-ease.