Skip to content
Cynosurev0.5.0
SearchStorybookGitHubnpmColour mode: System. Switch to Light

Components / Structure

VirtualList

A list of thousands of rows that stays fast: only the rows in view are rendered.

Ten thousand rows render only what is in view

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"

tsx
Copy
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.

Multiple selection

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.

tsx
Copy
<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))}
/>
Alt arrows move the selection

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().

tsx
Copy
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>
Follows new rows at the end

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().

tsx
Copy
<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>}
/>
At bottom change and overlay

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.

Section headings stay pinned

PropsLink to "Props"

Every prop of VirtualList
PropWhat it doesTypeDefault
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 with aria-posinset and aria-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.
Behaviour and accessibility, table 1
KeyDoes
Arrow Up / DownMove (skipping disabled rows)
Page Up / DownMove a screenful
Home / EndFirst / last row
SpaceChoose (toggle with multiple)
EnteronAction, or choose
Shift+arrowsExtend the selection (multiple)
Ctrl/Cmd+AChoose all (multiple)
Alt+Up / DownMove 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 empty and is no longer a listbox.
Empty

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).

ScrollArea · Tree · Table · SortableList

These examples run Cynosure 0.5.0 (commit 50ce30b), the version on npm.

Getting started
Theming
Icons
Accessibility
Code editors and highlighters
Using Cynosure without React
Build-tool adapters
Components
Accordion
AppShell
AspectRatio
Breadcrumbs
Calendar
Card
Container
Disclosure
Divider
DocumentViewport
Grid
Inline
PageHeader
PanZoomCanvas
ScrollArea
SideNav
SortableList
SplitPane
Stack
StatusBar
Tabs
TitleBar
Toolbar
Tree
VirtualList
ZoomControls
Announcer
Avatar
Badge
CodeBlock
DescriptionList
EmptyState
ErrorBoundary
Field
Heading
IconTile
Image
Kbd
List
LoadingDots
Notice
ProgressBar
Prose
Skeleton
Stat
Steps
Table
Text
Button
Checkbox
Checklist
ColorField
ColorSwatches
Combobox
CompareSlider
CopyButton
DateField
FileDrop
IconButton
KeyValueEditor
Link
NumberField
Pagination
PathField
RadioGroup
SearchField
SegmentedControl
Select
SelectionFrame
SignaturePad
Slider
Switch
TagInput
TextArea
TextField
ThemeToggle
TimeField
ButtonBase
ChoiceBase
DialogBase
FileInputBase
HiddenValue
Icon
InputBase
LinkBase
LiveRegion
TextAreaBase
ChatMessage
Choice
DataGrid
DeviceFrame
HighlightedText
HotkeyField
ListRow
OptionList
OverlayFrame
QRCode
RowActions
Section
SiteFooter
SiteHeader
SkipLink
SortButton
VisuallyHidden
CommandPalette
ContextMenu
Dialog
FloatingToolbar
HoverCard
Menu
Menubar
Popover
ProgressOverlay
Sheet
Toast
Tooltip
Changelog