OpenLand IconOpenLand
Back to Open Source

ink-scroll-view

TypeScript

A robust ScrollView and ScrollList component for Ink CLI applications.

ink-scroll-view

A robust, performance-optimized ScrollView component for Ink CLI applications.

License Version

✨ Features

  • šŸ“¦ Flexible Container: Handles content larger than the visible terminal viewport.
  • ⚔ Performance First:
    • Optimistic Updates: Immediate state updates for smoother interaction.
    • Efficient Re-rendering: Renders all children but strictly manages visibility via overflow and offsets, ensuring correct layout without layout thrashing.
  • šŸ“ Auto-Measurement: Automatically measures child heights using a virtually rendered DOM.
  • šŸ” Dynamic Content: Supports adding, removing, and expanding/collapsing items on the fly.
  • āš“ļø Layout Stability: Includes logic to maintain scroll position context when content changes.

šŸŽ¬ Demos

Scrolling

Scrolling Demo

Dynamic Items

Dynamic Items Demo

Expand/Collapse

Expand Demo

Resize

Resize Demo

Dynamic Width

Width Demo

šŸ“¦ Installation

npm install ink-scroll-view # Peer dependencies npm install ink react

šŸš€ Usage

ScrollView is a layout primitive. It does not capture user input automatically. You must control it programmatically using React refs and Ink's useInput.

import React, { useRef, useEffect } from "react"; import { render, Text, Box, useInput, useStdout } from "ink"; import { ScrollView, ScrollViewRef } from "ink-scroll-view"; const App = () => { const scrollRef = useRef<ScrollViewRef>(null); const { stdout } = useStdout(); // 1. Handle Terminal Resizing due to manual window change useEffect(() => { const handleResize = () => scrollRef.current?.remeasure(); stdout?.on("resize", handleResize); return () => { stdout?.off("resize", handleResize); }; }, [stdout]); // 2. Handle Keyboard Input useInput((input, key) => { if (key.upArrow) { scrollRef.current?.scrollBy(-1); // Scroll up 1 line } if (key.downArrow) { scrollRef.current?.scrollBy(1); // Scroll down 1 line } if (key.pageUp) { // Scroll up by viewport height const height = scrollRef.current?.getViewportHeight() || 1; scrollRef.current?.scrollBy(-height); } if (key.pageDown) { const height = scrollRef.current?.getViewportHeight() || 1; scrollRef.current?.scrollBy(height); } }); return ( <Box height={10} width="100%" borderStyle="single" borderColor="green" flexDirection="column" > <ScrollView ref={scrollRef}> {Array.from({ length: 50 }).map((_, i) => ( <Text key={i}>Item {i + 1} - content with variable length...</Text> ))} </ScrollView> </Box> ); }; render(<App />);

šŸ“ How it Works

The component renders all children into a container but shifts the content vertically using marginTop. The parent box with overflow="hidden" acts as the "viewport".

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ (hidden content) │ ← Content above viewport │ ... │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ ← scrollOffset (distance from top) │ ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā” │ │ │ Visible Viewport │ │ ← What user sees │ │ │ │ │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜ │ ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤ │ (hidden content) │ ← Content below viewport │ ... │ ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

šŸ“š API Reference

For detailed API documentation, see API Reference.

Props (ScrollViewProps)

Inherits standard BoxProps from Ink.

PropTypeDescription
childrenReactElement[]Required. List of child elements. Must use unique keys (strings/numbers).
onScroll(offset: number) => voidCalled when scroll position changes.
onViewportSizeChange(layout: { width, height }) => voidCalled when the viewport dimensions change.
onContentHeightChange(height: number) => voidCalled when the total content height changes.
onItemHeightChange(index: number, height: number) => voidCalled when an individual item's height changes.
...BoxPropsAny other prop accepted by Ink's Box.

Ref Methods (ScrollViewRef)

Access these via ref.current.

MethodSignatureDescription
scrollTo(offset: number) => voidScrolls to an absolute Y offset from the top.
scrollBy(delta: number) => voidScrolls by a relative amount (negative = up, positive = down).
scrollToTop() => voidHelper to scroll to offset 0.
scrollToBottom() => voidHelper to scroll to the maximum possible offset (contentHeight - viewportHeight).
getScrollOffset() => numberReturns the current scroll offset.
getContentHeight() => numberReturns the total height of all content items.
getViewportHeight() => numberReturns the current height of the visible area.
getBottomOffset() => numberReturns the scroll offset when scrolled to the bottom (contentHeight - viewportHeight).
getItemHeight(index: number) => numberReturns the measured height of a specific item by its index.
getItemPosition(index: number) => { top, height }Returns the position (top offset) and height of a specific item.
remeasure() => voidRe-checks viewport dimensions. Must call this on terminal resize.
remeasureItem(index: number) => voidForces a specific child to re-measure. Useful for dynamic content (expand/collapse) that doesn't trigger a full re-render.

šŸ’” Tips

  1. Unique Keys: Always provide stable, unique key props (strings or numbers) to your children. This allows ScrollView to accurately track height changes even when items are re-ordered or removed.
  2. Terminal Resizing: Ink components don't automatically know when the terminal window resizes. You need to listen to process.stdout's resize event and call remeasure() on the ref.
  3. Dynamic Content: If you have an item that expands (e.g., "See more"), calling remeasureItem(index) is more efficient than forcing a full update.

šŸ”— Related Packages

This package is part of a family of Ink scroll components:

PackageDescription
ink-scroll-viewCore scroll container component (this package)
ink-scroll-listA scrollable list component built on top of ink-scroll-view with focus management and item selection
ink-scroll-barA standalone scrollbar component that can be used with any scroll container

License

MIT