Components / Structure
VirtualList
A list of thousands of rows that stays fast: only the rows in view are rendered.
When to useLink to "When to use"
- Long lists of files, log lines, search results or history, where people pick one or several rows or open one.
Use something else when:
- the list is short (under a few hundred rows): Select, RadioGroup or a plain list
- rows nest: Tree (which windows the same way with
virtual) - rows have columns: Table
- people reorder rows: SortableList
UsageLink to "Usage"
import { VirtualList } from "@arshad-shah/cynosure";
<VirtualList
label="Files"
items={files}
getKey={(file) => file.id}
renderItem={(file) => file.name}
onAction={(file) => open(file)}
/>Rows share one height, set by CSS and measured, so keep each row to a line or a fixed layout. Set the list's height with style={{ blockSize: … }} or the height token.
ReorderingLink to "Reordering"
Give onMove to let people reorder, such as the pages of a document. Alt+Up/Down moves the chosen rows (or the focused row) one place; a mouse or pen drags them, with a line where they will land, scrolling near the edges, and Escape to cancel. Chosen rows move together. moveItems applies a move to your items.
<VirtualList
label="Pages"
items={pages}
getKey={(p) => p.id}
renderItem={(p) => p.name}
selectionMode="multiple"
onMove={(keys, to) => setPages(moveItems(pages, keys, to, (p) => p.id))}
/>Logs and sectionsLink to "Logs and sections"
wrap lets rows wrap and grow (each row is measured), and follow keeps the end in view as rows arrive while people are reading the latest. handle scrolls from outside: scrollToIndex(index, { align }) ("nearest", the default, "start" or "center", as ScrollArea's scrollIntoView) and scrollToEnd().
const log = useRef<VirtualListHandle>(null);
<VirtualList label="Requests" items={lines} getKey={(l) => l.id} renderItem={(l) => l.text} selectionMode="none" wrap follow handle={log} />
<Button onClick={() => log.current?.scrollToEnd()}>Latest</Button>onAtBottomChange reports when the view leaves the end (someone scrolled up to read) and comes back. overlay sits over the bottom centre of the list, outside the listbox, and does not scroll: put a "3 new" pill or a "Latest" button there that calls handle.scrollToEnd().
<VirtualList
label="Activity"
items={events}
getKey={(e) => e.id}
renderItem={(e) => e.text}
selectionMode="none"
follow
handle={log}
onAtBottomChange={setAtBottom}
overlay={!atBottom && unseen > 0 && <Button onClick={() => log.current?.scrollToEnd()}>{`${unseen} new`}</Button>}
/>isHeader marks rows that head a section (a date, a folder). They cannot be focused or chosen, and the current section's heading stays pinned at the top while its rows scroll under it.
PropsLink to "Props"
| Prop | What it does | Type | Default |
|---|---|---|---|
labelRequired | Accessible name of the list. | string | |
itemsRequired | The data, one entry per row. | readonly T[] | |
getKeyRequired | A stable, unique key per item. | (item: T, index: number) => string | |
renderItemRequired | The content of one row. Rows share one height unless wrap is set. | (item: T, state: VirtualItemState) => ReactNode | |
wrap | Rows wrap their text and take the height they need (log lines, messages); each is measured as it renders. | boolean | |
follow | Keep the end in view as rows are added, while the list is scrolled to the end (a log, a live feed). | boolean | |
onAtBottomChange | Called with false when the view leaves the end of the list and true when it is back, from scrolling or from rows arriving past it. It starts true. Use it to show a "jump to latest" control in overlay. | ((atBottom: boolean) => void) | |
overlay | Content pinned over the list at the bottom centre that does not scroll: a "3 new" pill, a "jump to latest" button. It is outside the listbox. | ReactNode | |
isHeader | Rows that head a section (a date, a group). They cannot be focused or selected, and the current section's heading stays pinned at the top while its rows scroll under it. | ((item: T) => boolean) | |
handle | scrollToIndex and scrollToEnd, for scrolling from outside the list. | Ref<VirtualListHandle> | |
isDisabled | Return true for rows that cannot be selected; the arrow keys skip them. | ((item: T) => boolean) | |
selectionMode | "none" (a list to act on), "single" (default) or "multiple". | "single" | "multiple" | "none" | |
selected | Keys of the selected rows (controlled). | string[] | |
defaultSelected | Keys of the rows selected at the start when uncontrolled. | string[] | |
selectedKeys | Old name of selected. | string[] | |
defaultSelectedKeys | Old name of defaultSelected. | string[] | |
onSelectionChange | Called with the selected keys when the selection changes. | ((keys: string[]) => void) | |
onAction | Enter on a row, a double-click, or a click when nothing is selectable. | ((item: T, index: number) => void) | |
onMove | Reordering: Alt+Up/Down moves the selection (or the focused row), and so does dragging it with a mouse or pen. keys are the rows moved, in order; to is where the first of them lands in the new order. moveItems applies it. | ((keys: string[], to: number) => void) | |
empty | Shown when there are no items. | ReactNode | |
labels | Replace the built-in English text: the announcement after a move. | Partial<VirtualListLabels> | |
size | Row height and text size. Default "md". | "sm" | "md" | "md" |
className | Added to the root element, for layout. | string | |
style | Layout only (e.g. blockSize). Restyle through --cy-virtual-list-* tokens. | CSSProperties |
Generated from the type definitions in @arshad-shah/cynosure 0.5.0.
Behaviour and accessibilityLink to "Behaviour and accessibility"
- Role: a listbox named by
label; rows are options witharia-posinsetandaria-setsize, so a screen reader says "File 3, 3 of 10,000". - Focus: one tab stop, on the chosen row or the first. The focused row is always rendered, so focus survives scrolling.
| Key | Does |
|---|---|
| Arrow Up / Down | Move (skipping disabled rows) |
| Page Up / Down | Move a screenful |
| Home / End | First / last row |
| Space | Choose (toggle with multiple) |
| Enter | onAction, or choose |
| Shift+arrows | Extend the selection (multiple) |
| Ctrl/Cmd+A | Choose all (multiple) |
| Alt+Up / Down | Move the chosen rows (with onMove) |
- Pointer: a click chooses (toggles with
multiple); a double-click acts. - Touch: rows are 44px tall on touch screens; a tap chooses.
- Empty: with no items the list shows
emptyand is no longer a listbox.
ThemingLink to "Theming"
Tokens: --cy-virtual-list-height, --cy-virtual-list-row-height, --cy-virtual-list-selected-background. The overlay is placed by OverlayFrame (--cy-overlay-frame-offset, --cy-overlay-frame-layer).
RelatedLink to "Related"
ScrollArea · Tree · Table · SortableList
These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.