Skip to content

A virtualized list component for Ink terminal applications. Only renders visible items for optimal performance with large datasets.

License

Notifications You must be signed in to change notification settings

archcorsair/ink-virtual-list

Repository files navigation

ink-virtual-list

A virtualized list component for Ink terminal applications. Only renders visible items for optimal performance with large datasets.

Features

  • 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

Installation

# npm
npm install ink-virtual-list

# jsr
npx jsr add @archcorsair/ink-virtual-list

# bun
bun add ink-virtual-list

Usage

Basic Example

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 (
    <VirtualList
      items={items}
      selectedIndex={selectedIndex}
      height={10}
      renderItem={({ item, isSelected }) => (
        <Text color={isSelected ? 'cyan' : 'white'}>
          {isSelected ? '> ' : '  '}
          {item}
        </Text>
      )}
    />
  );
}

Auto-fill Terminal Height

<VirtualList
  items={items}
  height="auto"
  reservedLines={5}  // Reserve space for header/footer
  renderItem={({ item }) => <Text>{item}</Text>}
/>

Custom Overflow Indicators

<VirtualList
  items={items}
  renderOverflowTop={(count) => <Text dimColor>{count} hidden</Text>}
  renderOverflowBottom={(count) => <Text dimColor>{count} hidden</Text>}
  renderItem={({ item }) => <Text>{item}</Text>}
/>

Imperative Scrolling

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>}
    />
  );
}

API

Props

Required

  • items: T[] - Array of items to render
  • renderItem: (props: RenderItemProps<T>) => ReactNode - 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

Ref Methods

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

Types

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
}

Advanced Example

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>
  );
}

License

MIT

About

A virtualized list component for Ink terminal applications. Only renders visible items for optimal performance with large datasets.

Topics

Resources

License

Stars

Watchers

Forks