Skip to Content
Docs

ink-scroll-view

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

License Version Downloads

Visit the Project Website.

📘 Documentation

Read the Documentation.

✨ 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

Try the interactive Showcase.

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
childrenReactNodeOptional. 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, height, previousHeight) => voidCalled when an individual item’s height changes.
debugbooleanOptional. If true, overflows content instead of hiding it (useful for debugging layout).
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.

Controlled Component (ControlledScrollView)

For advanced use cases where you need full control over the scroll state (e.g., synchronizing multiple views, animating transitions), you can use ControlledScrollView.

It accepts a scrollOffset prop instead of managing it internally.

import { ControlledScrollView } from "ink-scroll-view"; // ... const [offset, setOffset] = useState(0); return ( <ControlledScrollView scrollOffset={offset} // ... other props > {children} </ControlledScrollView> );

💡 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.

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

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

License

MIT