A scrollable list of clickable rows: the action / navigation counterpart to SelectableList. Compose List.Item rows directly inside a List.Root; each is a <button> (with onClick) or your own element via asChild (e.g. an <a>), with an optional current highlight. It shares SelectableList's look (styled like the MultiSelect popover, with rows that highlight on hover / selection as an inset, rounded pill inside a bordered bg-popover container) and suits an account switcher or an SSO provider picker. It renders every row by default; for long lists, swap Root → VirtualRoot to window them.
A minimal version (three rows, no current state):
The same three rows hand-built without List, with the same chrome and no virtualization (fine for short, static lists):
Composition
Rootrenders the rounded, scrollablebg-popovercontainer and itsItemchildren (all of them, non-virtualized). Reads each item'scurrent/disabledto style its row. Bound its height (max-h-*,h-*, ormin-h-0 flex-1in a flex parent) so long lists scroll.VirtualRootis the drop-in windowed variant: sameItemchildren, only the visible slice mounted. See Virtualization.Itemis a clickable row: a<button>by default (onClick), or your own element viaasChild.currentgives the tinted pill.ItemTitle/ItemDescriptionare optional styled spans for the emphasized title and de-emphasized sub-line.
Virtualization
List.Root renders every row: simplest, and right for the short lists most action/navigation menus are. For a long list (hundreds+ of rows), swap in List.VirtualRoot: it windows the same Item children via @tanstack/react-virtual, mounting only what's visible. The call site is otherwise identical: same children, same props. (@tanstack/react-virtual ships with every list entrypoint; it is small, a few kB gzipped, and does no windowing work until a VirtualRoot renders.)
On the server, VirtualRoot renders the first slice of rows, sized by estimateItemHeight and overscan. The rest mount after hydration measures the viewport. Root renders every row in the server HTML. If the whole list must be in that HTML (for find-in-page, or for a reader without JavaScript), pick Root.
Windowing trade-off. Off-screen rows aren't in the DOM, so Tab and browser find-in-page only reach mounted rows (arrow keys still span the full list; they mount the target row before focusing it). That is the right trade for a large enough list. Prefer
Rootuntil a list is actually long.
Polymorphism
Render rows as links by passing asChild and an <a>: the row chrome (hover pill, keyboard-focus tint) applies to your element, so clicking navigates. Rows compose freely: each one here is a MediaObject with the provider icon on the left and the name over a de-emphasized owner on the right (an SSO identity-provider picker, as a list rather than a stack of cards).
Accessibility
The list is a role="list" of role="listitem" rows (the tag is a <div>; per WAI-ARIA the role, not the tag, carries the semantics); each Item is a focusable <button> (or your asChild element) inside its row. It is a non-selecting list, not a selection widget; that's SelectableList's job. Set an aria-label (or aria-labelledby) on the Root so the list has an accessible name. The current row is announced via aria-current, not just tinted. Disabled rows are dimmed: a <button> row sets the native disabled, while an asChild row is conveyed with aria-disabled, removed from the tab order, and swallows clicks. For a genuinely disabled link, prefer omitting or removing the href.
Keyboard: rows keep their native tab order, and Arrow↑/Arrow↓ / Home / End also move focus between rows, skipping disabled ones (no wrap; arrowing past the last row holds). The focused row lights up with the same tint as hover instead of drawing a focus ring on the item, matching the SelectableList active row. Arrow navigation works under VirtualRoot too (a jump scrolls the target row into view and mounts it before moving focus), though off-screen rows still aren't reachable by Tab alone (see Virtualization).
API Reference
List.Root
The non-virtualized container. Accepts all <div> props, plus:
Bound the height (e.g. className="max-h-80") so long lists scroll.
List.VirtualRoot
The windowed container — same surface as Root (compose the same Item children), plus the virtualizer knobs:
List.Item
A clickable row. Accepts all <button> props (minus type), plus:
List.ItemTitle / List.ItemDescription
Styled <span>s for the title and de-emphasized sub-line inside an Item. Both accept all <span> props.