List
A general-purpose content list — a stack of peer item rows (leading media · title/description · trailing meta/actions) for settings, inbox/notification feeds, file lists, search results, and resource lists. Terse or composed items, surfaces & dividers, grouped sections, truncation, expandable rows, per-row status/unread/tone, and three selection models (navigational overlay, a keyboard GridList for multi-select, a radiogroup for single) with select-all, range, and a bulk-action bar.
@garn/ui/listDefault
Terse rows — leading icon + title + description + a trailing badge.
- Q3 financials.pdfUpdated 2 hours ago · 1.2 MBPDF
- hero-banner.pngUpdated yesterday · 4.8 MBPNG
- Brand assets24 itemsFolder
Appearances
plain · divided (inset) · card surfaces.
plain
- Starred12 items
- Archive340 items
- TrashEmpties in 30 days
divided · inset
- Starred12 items
- Archive340 items
- TrashEmpties in 30 days
card
- Starred12 items
- Archive340 items
- TrashEmpties in 30 days
Selection Multiple
A keyboard grid — checkboxes, select-all, range, and a bulk-action bar.
Selection Single
A radiogroup — single-choice rows (a plan picker).
- Starter$0 / mo
- Standard$12 / mo
- Pro$29 / mo
Grouped
List.Group section headers over nested lists.
- Today
- New comment on your PR2:14 PM
- Your post was featured11:03 AM
- Earlier
- Weekly digest readyMon
Sizes
The size ladder — xs (condensed) · sm · md · lg.
xs
- Documents18 items
- Photos204 items
sm
- Documents18 items
- Photos204 items
md
- Documents18 items
- Photos204 items
lg
- Documents18 items
- Photos204 items
Truncation
Title clamped to one line, description to N lines.
- A very long subject line that will be clamped to a single line with an ellipsis3
Loading Empty
List.Skeleton while loading; an Empty state via List.Empty.
loading
empty
- No items yet
Files you add will show up here.
Expandable
Expandable rows — a disclosure chevron reveals List.ItemDetail.
- Standard shipping3 items · $48.00Wireless mouse — $24.00USB-C cable — $12.00Notebook — $12.00
- Gift wrappingOptional add-onA recycled kraft wrap with a handwritten note, added at checkout.
Status
Per-row status — unread, saving (spinner), error (retry), and a tone accent.
- Unread:New followerMarked unreadnow
- report.pdfSaving changes…
- banner.pngUpload failed
- deploy.ymlDeployed to productionLive
Keyboard Grid
A navigable link list (keyboardNavigation) — arrow keys + type-ahead.
Compose In
Compose-in — a right-click ContextMenu and an InlineEdit title.
- Right-click for actions · click the title to rename
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add list- Registry
list- Source
- list.tsx
- Deps
@radix-ui/react-slotlucide-react- Registry deps
utilsuse-controllable-stateuse-controllable-key-setuse-merged-refsortablecheckboxspinner
Import
import { List } from "@garn/ui/list";Props
Generated from the component's TypeScript types.
appearance- Description
- Surface treatment: bare rows, hairline dividers, or one bordered card.
- Type
"card" | "divided" | "plain"- Default
plain
selectionMode- Description
- Selection model. `single` → radiogroup; `multiple` → keyboard grid.
- Type
"multiple" | "none" | "single"- Default
none
size- Description
- Row height / type ladder — `xs` is a condensed, denser row (distinct from `density`).
- Type
"lg" | "md" | "sm" | "xs"- Default
md
defaultSelectedKeys- Description
- Uncontrolled initial selection.
- Type
string[]
density- Description
- Local density override; omit to inherit the ambient `[data-density]`.
- Type
"compact" | "spacious"
descriptionLines- Description
- Lines a clamped description shows before ellipsis (default 2).
- Type
number
disabledKeys- Description
- Keys that cannot be selected (skipped by select-all and range).
- Type
string[]
inset- Description
- Align dividers past the leading gutter (only with `divided`/`card`).
- Type
boolean
itemValues- Description
- The item keys in order, for reorderable lists whose rows are WRAPPED in another component (child introspection only sees direct `List.Item`s — without this a wrapper-composed list reorders against an empty order).
- Type
string[]
keyboardNavigation- Description
- Opt a non-selectable list into the roving keyboard grid (e.g. a link list).
- Type
boolean
label- Description
- Convenience accessible name for the list.
- Type
React.ReactNode
onReorder- Description
- Called with the next key order when a reorder drops. You reorder your data.
- Type
(keys: string[]) => void
onSelectionChange- Description
- Called with the next selection whenever it changes.
- Type
(keys: string[]) => void
reorderable- Description
- Enable drag-reorder (each `List.Item` needs a `value`; add a `List.DragHandle`). Powered by the `useSortable` primitive — keyboard + pointer + announcements.
- Type
boolean
selectedKeys- Description
- Controlled selected keys (each `List.Item` needs a `value`).
- Type
string[]
truncate- Description
- Ambient truncation: clamp titles to 1 line and descriptions to `descriptionLines`.
- Type
boolean
Plus 279 inherited native <ul> attributes.
List.Item
actions- Description
- Terse mode: trailing focusable controls (kept above the row overlay).
- Type
React.ReactNode
defaultExpanded- Description
- Uncontrolled initial expanded state.
- Type
boolean
description- Description
- Terse mode: the secondary text.
- Type
React.ReactNode
detail- Description
- Terse mode: disclosure content revealed when the row is expanded.
- Type
React.ReactNode
disabled- Description
- Inert, non-selectable row.
- Type
boolean
expandable- Description
- Show a disclosure toggle that reveals `detail` / `List.ItemDetail`.
- Type
boolean
expanded- Description
- Controlled expanded state (pair with `onExpandedChange`).
- Type
boolean
href- Description
- Make the whole row a navigational link (the overlay pattern).
- Type
string
leading- Description
- Terse mode: the leading media (icon / avatar / thumbnail).
- Type
React.ReactNode
onActivate- Description
- Make the whole row activate on click (rendered as a button overlay).
- Type
(event: React.MouseEvent) => void
onExpandedChange- Description
- Called when the disclosure opens or closes.
- Type
(expanded: boolean) => void
status- Description
- Per-row liveness: loading/saving show a spinner; error tints the row danger.
- Type
"error" | "idle" | "loading" | "saving"
title- Description
- Terse mode: the primary text (becomes the row's link when interactive).
- Type
React.ReactNode
tone- Description
- Status tone accent for the row (a leading bar).
- Type
"brand" | "danger" | "info" | "neutral" | "success" | "warning"
trailing- Description
- Terse mode: inert trailing metadata (timestamp / badge / count).
- Type
React.ReactNode
unread- Description
- Emphasise an unread row (bold title + a leading dot).
- Type
boolean
value- Description
- Stable selection key — required for a row to participate in selection.
- Type
string
Plus 277 inherited native HTML attributes.
List.Title
asChild- Description
- Render as the child element (e.g. a link, button, or heading), merging props.
- Type
boolean
Plus 280 inherited native HTML attributes.
List.Description
lines- Description
- Clamp to N lines (default from the List's `descriptionLines`).
- Type
number
truncate- Description
- Clamp to a single line with an ellipsis.
- Type
boolean
Plus 280 inherited native HTML attributes.
List.Group
label- Description
- Section header text.
- Type
React.ReactNode
sticky- Description
- Stick the header to the top of the scroll container while its run is visible.
- Type
boolean
Plus 281 inherited native HTML attributes.
List.SelectionBar
clearLabel- Description
- Label for the clear-selection button (default "Clear").
- Type
string
renderCount- Description
- Label template — `count` is the selected total (default "N selected").
- Type
(count: number) => React.ReactNode
selectAllLabel- Description
- Label for the select-all control (default "Select all").
- Type
string
Plus 280 inherited native HTML attributes.
List.Skeleton
leading- Description
- Show a leading media placeholder circle.
- Type
boolean
lines- Description
- Description placeholder lines (default 1).
- Type
number
rows- Description
- How many placeholder rows to render (default 3).
- Type
number
size- Description
- No description.
- Type
ListSize
Plus 280 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="list"][data-slot="list-label"][data-slot="list-row"][data-slot="list-selection"][data-slot="list-unread"][data-slot="list-disclosure"][data-slot="list-status"]| Property | Token | Tier |
|---|---|---|
rowGap | --garn-gap-stack | semantic · air |
rowHeight | --garn-control-h-md | component · identity |
surfaceRadius | --garn-radius | semantic |
toneAccent | --garn-badge-neutral-boldbrandable | component |
Private instance vars (never externalize): --list-inset
Overview
List is the plain content collection — a stack of peer rows, each a leading-media · title/description · trailing-meta/actions row. List.Item is one row (terse props or composed slots); reach for it for settings rows, inbox/notification feeds, file lists, search results, and member/resource lists. It scales from an inert list to navigational rows (the whole row is one link) to full multi-select (a keyboard grid with select-all, range, and a bulk-action bar) — chosen by selectionMode, not a pile of booleans. For a term→value detail block use DataList; for a chronology use Timeline; for aligned columns of cells use Table; for a hierarchical tree use Tree.
Guidelines
Pick the interaction model deliberately. Leave rows inert for a read-only list; set href/onActivate for a navigable list (the whole row becomes one target); set selectionMode for selection — single is a radiogroup (a form choice), multiple is a keyboard grid (bulk actions). Give every selectable row a stable value.
Use the axes, not ad-hoc styling. appearance (plain/divided/card) sets the surface; size sets the row identity (xs for dense inbox/log rows); density tightens ambient spacing. Split inert metadata (List.Trailing) from focusable controls (List.Actions) so the tab model stays honest.
Keep it accessible. Name the list; don't nest interactives inside a navigational row's overlay — put secondary actions in List.Actions (they escape it); convey status with text/tone plus the sr-only cues, never colour alone. Long lists reorder via reorderable and virtualize via a windowing-library recipe, not built-in props.
Best practices
- Give the list an accessible name (label / aria-label).
- Give every selectable row a stable
value; drive selection with selectedKeys / onSelectionChange (or defaultSelectedKeys). - Put inert meta in List.Trailing and focusable controls in List.Actions (they escape the navigational overlay).
- Use
size="xs"for a condensed, dense list; usedensityfor ambient tightening — they're different axes. - Group long lists with List.Group; show List.Skeleton while loading and an <Empty> (via List.Empty) when cleared.
- Don't build a navigational row by wrapping the whole <li> in an <a> — use List.Title asChild (the overlay pattern) so nested actions stay valid.
- Don't forget
valueon selectable rows (they can't be selected or keyed without it). - Don't rely on a row's
tone/colour alone for status — pair it with text and the sr-only cues. - Don't reach for List for a tree, a table, or a term:value block — use Tree / Table / DataList.
Content guidelines
- Lead the row with the title (what it is); keep the description terse and let it truncate.
- Keep trailing metadata short (a time, a badge, a count); move anything actionable into List.Actions.
Accessibility
- Role
list- Focus
- Inert/navigational rows aren't focusable — only the title link and List.Actions are. The keyboard grid puts a roving tabindex on the rows (active row = 0, others = -1) and roves the active row's in-row controls; the radiogroup roves its radios. Every focusable control keeps a 2px --garn-ring focus ring.
Enforceable contracts
Give the List an accessible name via `label`, `aria-label`, or `aria-labelledby`.
when a List has no `label`, `aria-label`, or `aria-labelledby`
Give every selectable List.Item a stable `value` so it can be selected and keyed.
when selectionMode is not `none` and a List.Item has no `value`
- • Presentational / navigational lists are a real <ul role=list> of <li role=listitem>; a navigational row uses the 'card-with-primary-action' overlay — List.Title becomes the sole <a>/<button> with an ::after spanning the row (one tab stop) while List.Actions sit above the overlay with their own tab stops (no nested interactives).
- • `selectionMode="multiple"` (and `keyboardNavigation`) is a real ARIA grid: a <div role=grid aria-multiselectable> of <div role=row aria-selected> rows, each wrapping a <div role=gridcell>. A grid CANNOT be built from <ul>/<li> because ARIA forbids role=row on <li>, so grid mode forks the element types.
- • The grid roves row focus with a roving tabindex (one row tabbable at a time): Arrow Up/Down, Home/End, PageUp/PageDown move focus; type-ahead jumps to a title; Space toggles the focused row; Shift+Arrow / Shift+Click extend a range; ⌘/Ctrl+A selects all; Escape clears; Enter activates a navigational row. In-row controls (checkbox, link, actions) rove with the active row so Tab steps through the active row only.
- • `selectionMode="single"` flips the <ul> to role=radiogroup, the <li> wrappers take role=presentation, and each row renders a roving role=radio (Arrow keys move + select). It is a single-choice form control, not a grid.
- • The bulk-action bar (List.SelectionBar) has a tri-state select-all Checkbox and announces the selected count via a polite live region; disabled keys are skipped by select-all and range.
- • An expandable row's disclosure is a real <button aria-expanded aria-controls>; the revealed List.ItemDetail is wired by aria-controls (no role=region — an unnamed region is an anti-pattern).
- • Per-row status: loading/saving set aria-busy and show a Spinner; the row tone is a decorative accent (error implies the danger tone) — never the sole signal. Give the list an accessible name via `label`, `aria-label`, or `aria-labelledby` (dev-warns if missing).
- Status
- Stable
- Version
v0.3.0- Introduced
v0.2.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.