List
A scrollable list container with keyboard navigation, highlight, selection and scroll-bottom callbacks
Installation
Usage
Basic Usage
A scroll container driven by items; rows are rendered via renderItem. Pair it with the usePagination hook when paging is needed.
"use client";import { List } from "@/ui";interface Task { id: number; title: string; status: string;}const tasks: Task[] = [ { id: 1, title: "Design review", status: "todo" }, { id: 2, title: "API integration", status: "in-progress" }, { id: 3, title: "Write tests", status: "in-progress" }, { id: 4, title: "Release notes", status: "done" }, { id: 5, title: "Deploy", status: "todo" }, { id: 6, title: "Retrospective", status: "todo" },];export default function ListBasicDemo() { return ( <List className="h-40 w-full max-w-md border" items={tasks} getKey={(task) => task.id} empty={<div className="px-3 py-4 text-center text-muted-foreground">No data</div>} classNames={{ item: "flex items-center gap-2" }} renderItem={(task) => ( <> <span className="flex-1 truncate">{task.title}</span> <span className="text-muted-foreground">{task.status}</span> </> )} /> );}Keyboard navigation is built in: the root is focusable (tabIndex={0}), ArrowUp / ArrowDown move the highlight (clamped at both ends) and scroll it into view automatically, and Enter or a click on a row fires onSelect. The highlighted item carries data-highlighted (styled as bg-hover). For containers that keep focus elsewhere — such as a combobox input — pass highlightIndex / onHighlightChange to control the highlight externally; see the Combobox guide.
Ordered List
Order renders the same list as an <ol> with decimal numbering — for sequences where order is part of the meaning.
"use client";import { Order } from "@/ui";interface Step { id: number; title: string; owner: string;}const steps: Step[] = [ { id: 1, title: "Freeze the release branch", owner: "Alice" }, { id: 2, title: "Run the regression suite", owner: "Bob" }, { id: 3, title: "Write release notes", owner: "Carol" }, { id: 4, title: "Tag and deploy", owner: "Alice" }, { id: 5, title: "Announce in the changelog", owner: "Bob" },];export default function ListOrderDemo() { return ( <Order className="h-40 w-full max-w-md border" items={steps} getKey={(step) => step.id} empty={<div className="px-3 py-4 text-center text-muted-foreground">No steps</div>} classNames={{ item: "flex items-center gap-2" }} renderItem={(step) => ( <> <span className="flex-1 truncate">{step.title}</span> <span className="text-muted-foreground">{step.owner}</span> </> )} /> );}Grouping
Provide getGroup to derive a group label per item; a header row is rendered when the label changes. Group headers are plain rows — they are not focusable, not highlighted by keyboard navigation and not selectable, so ArrowUp / ArrowDown / Enter only ever operate on data items. Customize the header via renderGroupHeader.
Selected: - — group headers are not navigable or selectable
"use client";import { useState } from "react";import { List } from "@/ui";const contacts = [ { name: "Alice", role: "Engineering" }, { name: "Bob", role: "Engineering" }, { name: "Carol", role: "Design" }, { name: "David", role: "Design" }, { name: "Erin", role: "Marketing" },];export default function Demo() { const [selected, setSelected] = useState<string | null>(null); return ( <div className="flex flex-col gap-3 w-xs"> <List className="h-64 rounded-md border" items={contacts} getKey={(contact) => contact.name} getGroup={(contact) => contact.role} renderGroupHeader={(group) => group} classNames={{ item: "flex items-center gap-2" }} onSelect={(contact) => setSelected(contact.name)} renderItem={(contact) => ( <> <span className="flex-1 truncate">{contact.name}</span> <span className="text-muted-foreground">{contact.role}</span> </> )} /> <p className="text-sm text-muted-foreground wrap-break-word"> Selected: {selected ?? "-"} — group headers are not navigable or selectable </p> </div> );}API Reference
High-level Components
List
A scrollable, keyboard-navigable list; sizing is fully controlled by the consumer (e.g. h-40 rounded-md border).
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | - | Data array to render |
renderItem | (item: T, index: number) => ReactNode | - | Row content renderer |
getKey | (item: T, index: number) => Key | - | Row key extractor; defaults to the index |
empty | React.ReactNode | - | Rendered when items is empty |
highlightIndex | number | null | - | Controlled highlight index; omit for internal keyboard navigation |
onHighlightChange | (index: number | null) => void | - | Fired when the highlight changes |
getGroup | (item: T, index: number) => string | - | Group label extractor; grouping is enabled only when both getGroup and renderGroupHeader are provided |
renderGroupHeader | (groupName: string) => ReactNode | - | Group header renderer; grouping is enabled only when both getGroup and renderGroupHeader are provided |
onSelect | (item: T, index: number) => void | - | Fired when a row is clicked or confirmed with Enter during keyboard navigation |
onScrollBottom | () => void | - | Fired when the list is scrolled to within 16px of the bottom, for load-more scenarios |
ref | React.Ref<HTMLUListElement | HTMLDivElement> | - | Ref to the root element — a ul in flat mode, a div when grouped, e.g. to move focus into the list |
className | ClassNameValue | - | Custom class for the root |
style | React.CSSProperties | - | Custom inline style for the root |
classNames | { item?: ClassNameValue; groupHeader?: ClassNameValue } | - | Custom classes for the rows and group headers |
styles | { item?: React.CSSProperties; groupHeader?: React.CSSProperties } | - | Custom inline styles for the rows and group headers |
Rows are contiguous — no built-in dividers (add divide-y divide-border via className if you want them). The list renders as a plain scroll container: the native scrollbar is hidden, scrolling is contained (overscroll-behavior: contain) so wheel/keyboard never chain-scrolls the page, and the container is keyboard-focusable. Pair it with ScrollShadow yourself when you want gradient edge indicators — compose them via className/children, the list ships no shadows. Keyboard: ArrowUp / ArrowDown move the highlight, Enter or Space selects it.
Order
An ordered-list variant that renders the root as an <ol> with decimal numbering (list-decimal pl-6). It accepts all List props except the grouping API (getGroup / renderGroupHeader), and its ref is React.Ref<HTMLOListElement>.
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | - | Data array to render |
renderItem | (item: T, index: number) => ReactNode | - | Row content renderer |
getKey | (item: T, index: number) => Key | - | Row key extractor; defaults to the index |
empty | React.ReactNode | - | Rendered when items is empty |
highlightIndex | number | null | - | Controlled highlight index; omit for internal keyboard navigation |
onHighlightChange | (index: number | null) => void | - | Fired when the highlight changes |
onSelect | (item: T, index: number) => void | - | Fired when a row is clicked or confirmed with Enter during keyboard navigation |
onScrollBottom | () => void | - | Fired when the list is scrolled to within 16px of the bottom, for load-more scenarios |
ref | React.Ref<HTMLOListElement> | - | Ref to the root <ol> element |
className | ClassNameValue | - | Custom class for the root |
style | React.CSSProperties | - | Custom inline style for the root |
classNames | { item?: ClassNameValue; groupHeader?: ClassNameValue } | - | Custom classes for each part (groupHeader is unused in Order) |
styles | { item?: React.CSSProperties; groupHeader?: React.CSSProperties } | - | Custom inline styles for each part (groupHeader is unused in Order) |