A virtualized list component for Ink terminal applications
npm install ink-virtual-listA virtualized list component for Ink terminal applications. Only renders visible items for optimal performance with large datasets.
- Virtualized rendering - Only renders items visible in the viewport
- Automatic scrolling - Keeps selected item in view as you navigate
- Terminal-aware - Responds to terminal resize events
- Flexible height - Fixed height or auto-fill available terminal space
- Customizable indicators - Override default overflow indicators ("▲ N more")
- TypeScript first - Full type safety with generics
- Imperative API - Programmatic scrolling via ref
``bashnpm
npm install ink-virtual-list
Usage
$3
`tsx
import { VirtualList } from 'ink-virtual-list';
import { Text } from 'ink';
import { useState } from 'react';function App() {
const [selectedIndex, setSelectedIndex] = useState(0);
const items = Array.from({ length: 1000 }, (_, i) =>
Item ${i + 1}); return (
items={items}
selectedIndex={selectedIndex}
height={10}
renderItem={({ item, isSelected }) => (
{isSelected ? '> ' : ' '}
{item}
)}
/>
);
}
`$3
`tsx
items={items}
height="auto"
reservedLines={5} // Reserve space for header/footer
renderItem={({ item }) => {item} }
/>
`$3
`tsx
items={items}
renderOverflowTop={(count) => ↑ {count} hidden }
renderOverflowBottom={(count) => ↓ {count} hidden }
renderItem={({ item }) => {item} }
/>
`$3
`tsx
import { useRef } from 'react';
import type { VirtualListRef } from 'ink-virtual-list';function App() {
const listRef = useRef(null);
const scrollToTop = () => {
listRef.current?.scrollToIndex(0, 'top');
};
return (
ref={listRef}
items={items}
renderItem={({ item }) => {item} }
/>
);
}
`API
$3
#### Required
-
items: T[] - Array of items to render
- renderItem: (props: RenderItemProps - Render function for each visible item
- Receives: { item: T, index: number, isSelected: boolean }#### Optional
-
selectedIndex?: number - Index of currently selected item (default: 0)
- keyExtractor?: (item: T, index: number) => string - Custom key extractor for list items
- height?: number | "auto" - Fixed height in lines or "auto" to fill terminal (default: 10)
- reservedLines?: number - Lines to reserve when using height="auto" (default: 0)
- itemHeight?: number - Height of each item in lines (default: 1)
- showOverflowIndicators?: boolean - Show "N more" indicators (default: true)
- renderOverflowTop?: (count: number) => ReactNode - Custom top overflow indicator
- renderOverflowBottom?: (count: number) => ReactNode - Custom bottom overflow indicator
- renderScrollBar?: (viewport: ViewportState) => ReactNode - Custom scrollbar renderer
- onViewportChange?: (viewport: ViewportState) => void - Callback when viewport changes$3
`typescript
interface VirtualListRef {
scrollToIndex: (index: number, alignment?: 'auto' | 'top' | 'center' | 'bottom') => void;
getViewport: () => ViewportState;
remeasure: () => void;
}
`-
scrollToIndex(index, alignment?) - Scroll to bring an index into view
- 'auto' (default) - Only scroll if needed
- 'top' - Align item to top of viewport
- 'center' - Center item in viewport
- 'bottom' - Align item to bottom of viewport
- getViewport() - Get current viewport state ({ offset, visibleCount, totalCount })
- remeasure() - Force recalculation of viewport dimensions$3
`typescript
interface RenderItemProps {
item: T;
index: number;
isSelected: boolean;
}interface ViewportState {
offset: number; // Items scrolled past
visibleCount: number; // Items currently visible
totalCount: number; // Total items
}
`Advanced Example
`tsx
import { VirtualList } from 'ink-virtual-list';
import { Box, Text } from 'ink';
import { useRef, useState } from 'react';
import type { VirtualListRef } from 'ink-virtual-list';interface Todo {
id: string;
title: string;
completed: boolean;
}
function TodoApp() {
const [todos] = useState([
{ id: '1', title: 'Learn Ink', completed: true },
{ id: '2', title: 'Build CLI', completed: false },
// ... 1000s more
]);
const [selectedIndex, setSelectedIndex] = useState(0);
const listRef = useRef(null);
return (
My Todos ({todos.length})
ref={listRef}
items={todos}
selectedIndex={selectedIndex}
height="auto"
reservedLines={3}
keyExtractor={(todo) => todo.id}
renderItem={({ item, isSelected }) => (
{isSelected ? '❯ ' : ' '}
{item.completed ? '✓' : '○'} {item.title}
)}
/>
{selectedIndex + 1} / {todos.length}
);
}
``MIT