A 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
# npm
npm install ink-virtual-list
# jsr
npx jsr add @archcorsair/ink-virtual-list
# bun
bun add ink-virtual-listimport { 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 (
<VirtualList
items={items}
selectedIndex={selectedIndex}
height={10}
renderItem={({ item, isSelected }) => (
<Text color={isSelected ? 'cyan' : 'white'}>
{isSelected ? '> ' : ' '}
{item}
</Text>
)}
/>
);
}<VirtualList
items={items}
height="auto"
reservedLines={5} // Reserve space for header/footer
renderItem={({ item }) => <Text>{item}</Text>}
/><VirtualList
items={items}
renderOverflowTop={(count) => <Text dimColor>↑ {count} hidden</Text>}
renderOverflowBottom={(count) => <Text dimColor>↓ {count} hidden</Text>}
renderItem={({ item }) => <Text>{item}</Text>}
/>import { useRef } from 'react';
import type { VirtualListRef } from 'ink-virtual-list';
function App() {
const listRef = useRef<VirtualListRef>(null);
const scrollToTop = () => {
listRef.current?.scrollToIndex(0, 'top');
};
return (
<VirtualList
ref={listRef}
items={items}
renderItem={({ item }) => <Text>{item}</Text>}
/>
);
}items: T[]- Array of items to renderrenderItem: (props: RenderItemProps<T>) => ReactNode- Render function for each visible item- Receives:
{ item: T, index: number, isSelected: boolean }
- Receives:
selectedIndex?: number- Index of currently selected item (default:0)keyExtractor?: (item: T, index: number) => string- Custom key extractor for list itemsheight?: number | "auto"- Fixed height in lines or"auto"to fill terminal (default:10)reservedLines?: number- Lines to reserve when usingheight="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 indicatorrenderOverflowBottom?: (count: number) => ReactNode- Custom bottom overflow indicatorrenderScrollBar?: (viewport: ViewportState) => ReactNode- Custom scrollbar rendereronViewportChange?: (viewport: ViewportState) => void- Callback when viewport changes
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
interface RenderItemProps<T> {
item: T;
index: number;
isSelected: boolean;
}
interface ViewportState {
offset: number; // Items scrolled past
visibleCount: number; // Items currently visible
totalCount: number; // Total items
}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<Todo[]>([
{ id: '1', title: 'Learn Ink', completed: true },
{ id: '2', title: 'Build CLI', completed: false },
// ... 1000s more
]);
const [selectedIndex, setSelectedIndex] = useState(0);
const listRef = useRef<VirtualListRef>(null);
return (
<Box flexDirection="column">
<Text bold>My Todos ({todos.length})</Text>
<VirtualList
ref={listRef}
items={todos}
selectedIndex={selectedIndex}
height="auto"
reservedLines={3}
keyExtractor={(todo) => todo.id}
renderItem={({ item, isSelected }) => (
<Box>
<Text color={isSelected ? 'cyan' : 'white'}>
{isSelected ? '❯ ' : ' '}
{item.completed ? '✓' : '○'} {item.title}
</Text>
</Box>
)}
/>
<Text dimColor>
{selectedIndex + 1} / {todos.length}
</Text>
</Box>
);
}MIT