Skip to content
Updated April 2026Edit this page ↗

VirtualList

A scroll-virtualized list that renders only the rows currently visible in the viewport. The rest of the dataset, whether 1,000 items or 10,000,000, never gets painted.

Scroll performance stays constant no matter how big the data gets. Reach for this whenever a list could grow beyond what fits on screen: log viewers, process tables, file pickers, search results.

Installation

TYPESCRIPT
npm install @termuijs/widgets

Basic Usage

TYPESCRIPT
import { VirtualList } from '@termuijs/widgets'
import { App } from '@termuijs/core'
 
const list = new VirtualList({
    totalItems: 100_000,
    renderItem: (index) => `Row \${index}: some content`,
    onSelect:   (index) => console.log('Selected:', index),
})
 
const app = new App(list, { fullscreen: true })
 
app.events.on('key', (e) => {
    if (e.key === 'up')   list.selectPrev()
    if (e.key === 'down') list.selectNext()
    if (e.key === 'enter') list.confirm()
})
 
await app.mount()
// ┌──────────────────────────────────────────────────────────────┐
// │   Row 0: some content                                    ░   │
// │ ▸ Row 1: some content                                    █   │
// │   Row 2: some content                                    ░   │
// │   Row 3: some content                                    ░   │
// └──────────────────────────────────────────────────────────────┘

Options

OptionTypeDefaultDescription
totalItemsnumberTotal number of items in the dataset
renderItem(index: number) => stringCalled for each visible item. Return its text content.
itemHeightnumber1Rows each item occupies
onSelect(index: number) => voidundefinedCalled when the user presses Enter
overscannumber2Extra items rendered above/below the viewport (prevents flicker on scroll)
showScrollbarbooleantrueShow a scrollbar indicator on the right
stylePartial<Style>undefinedStyle overrides (color, background, etc.)

Keyboard Navigation

Hook the list's methods to your key handler using app.events.on('key'):

TYPESCRIPT
import { App } from '@termuijs/core'
import { VirtualList } from '@termuijs/widgets'
 
const list = new VirtualList({ totalItems: 1000, renderItem: (i) => `Item \${i}` })
const app = new App(list, { fullscreen: true })
 
app.events.on('key', (e) => {
    if (e.key === 'up')       list.selectPrev()
    if (e.key === 'down')     list.selectNext()
    if (e.key === 'pageup')   list.pageUp()
    if (e.key === 'pagedown') list.pageDown()
    if (e.key === 'home')     list.selectFirst()
    if (e.key === 'end')      list.selectLast()
    if (e.key === 'enter')    list.confirm()
})
 
await app.mount()

Methods

MethodDescription
selectNext()Move selection down by one item
selectPrev()Move selection up by one item
selectFirst()Jump to the first item
selectLast()Jump to the last item
pageUp()Move up by one viewport height
pageDown()Move down by one viewport height
scrollTo(index)Jump to a specific index
confirm()Trigger onSelect with the current index

Data

MethodDescription
setTotalItems(count)Update the dataset size (e.g., after a filter or load). Clamps selection if needed.
setRenderItem(fn)Replace the render function (e.g., when data shape changes). Triggers a repaint.

Properties

PropertyTypeDescription
totalItemsnumberCurrent total item count
selectedIndexnumberCurrent selection (0-based)
scrollOffsetnumberFirst visible item index

Real-World Example: Filterable List

TYPESCRIPT
import { App } from '@termuijs/core'
import { VirtualList, TextInput, Box } from '@termuijs/widgets'
 
const ALL_PROCESSES = await getProcessList()   // 10,000+ items
 
let filtered = ALL_PROCESSES
 
const list = new VirtualList({
    totalItems: filtered.length,
    renderItem: (i) => {
const p = filtered[i]
return `\${p.pid.toString().padEnd(6)} \${p.name.padEnd(20)} \${p.cpu}%`
    },
    onSelect: (i) => inspectProcess(filtered[i]),
})
 
const searchInput = new TextInput({
    placeholder: 'Filter processes...',
    onChange: (query) => {
filtered = ALL_PROCESSES.filter((p) =>
p.name.toLowerCase().includes(query.toLowerCase())
)
list.setTotalItems(filtered.length)
list.selectFirst()
    },
})
 
const layout = new Box({ flexDirection: 'column' })
layout.addChild(searchInput)
layout.addChild(list)
 
const app = new App(layout, { fullscreen: true })
 
app.events.on('key', (e) => {
    if (e.key === 'up')    list.selectPrev()
    if (e.key === 'down')  list.selectNext()
    if (e.key === 'enter') list.confirm()
})
 
await app.mount()

Loading Async Data

Start with a placeholder count and update as data arrives:

TYPESCRIPT
const items: Item[] = []
 
const list = new VirtualList({
    totalItems: 0,
    renderItem: (i) => items[i]?.name ?? 'Loading...',
})
 
const app = new App(list, { fullscreen: true })
await app.mount()
 
// Data comes in pages
async function loadPage(offset: number) {
    const page = await fetchItems(offset, 50)
    items.push(...page)
    list.setTotalItems(items.length)
}
 
loadPage(0)

How Virtualization Works

On each render, VirtualList calculates which indices fall within the current viewport:

TYPESCRIPT
start = scrollOffset - overscan
end   = scrollOffset + visibleCount + overscan
 
// Anything outside [start, end] is never called or painted.
// totalItems only affects the scrollbar ratio and clamp logic.

The scrollbar position is computed as a ratio of scrollOffset / (totalItems - visibleCount). It uses block characters ( for thumb, for track) and disappears when all items fit in the viewport.

Performance

Dataset sizeItems rendered per frameMemory for item state
100 items~26 rows + 4 overscanO(viewport)
100,000 items~26 rows + 4 overscanO(viewport)
1,000,000 items~26 rows + 4 overscanO(viewport)

The only work that scales with dataset size is your renderItem function. keep it cheap. Derive display strings ahead of time if the computation is expensive.

See also

  • List: Simple non-virtualized list for small datasets (<100 items)
  • Table: Tabular data with column headers and alignment
  • TextInput: Combine with VirtualList for search/filter UIs
  • @termuijs/store: Manage filter/selection state outside the widget