A robust, performance-optimized ScrollView component for Ink CLI applications.
Visit the Project Website.
Read the Documentation.
- 📦 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
overflowand 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.
Try the interactive Showcase.
npm install ink-scroll-view
# Peer dependencies
npm install ink reactScrollView is a layout primitive. It does not capture user input automatically. You must control it programmatically using React refs and Ink's useInput.
importReact,{useRef,useEffect}from"react";import{render,Text,Box,useInput,useStdout}from"ink";import{ScrollView,ScrollViewRef}from"ink-scroll-view";constApp=()=>{constscrollRef=useRef<ScrollViewRef>(null);const{ stdout }=useStdout();// 1. Handle Terminal Resizing due to manual window changeuseEffect(()=>{consthandleResize=()=>scrollRef.current?.remeasure();stdout?.on("resize",handleResize);return()=>{stdout?.off("resize",handleResize);};},[stdout]);// 2. Handle Keyboard InputuseInput((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 heightconstheight=scrollRef.current?.getViewportHeight()||1;scrollRef.current?.scrollBy(-height);}if(key.pageDown){constheight=scrollRef.current?.getViewportHeight()||1;scrollRef.current?.scrollBy(height);}});return(<Boxheight={10}width="100%"borderStyle="single"borderColor="green"flexDirection="column"><ScrollViewref={scrollRef}>{Array.from({length: 50}).map((_,i)=>(<Textkey={i}>Item {i+1} - content with variable length...</Text>))}</ScrollView></Box>);};render(<App/>);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
│ ... │
└─────────────────────────┘
For detailed API documentation, see API Reference.
Inherits standard BoxProps from Ink.
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Optional. List of child elements. Must use unique keys (strings/numbers). |
onScroll | (offset: number) => void | Called when scroll position changes. |
onViewportSizeChange | (layout: { width, height }) => void | Called when the viewport dimensions change. |
onContentHeightChange | (height: number) => void | Called when the total content height changes. |
onItemHeightChange | (index, height, previousHeight) => void | Called when an individual item's height changes. |
debug | boolean | Optional. If true, overflows content instead of hiding it (useful for debugging layout). |
| ... | BoxProps | Any other prop accepted by Ink's Box. |
Access these via ref.current.
| Method | Signature | Description |
|---|---|---|
scrollTo | (offset: number) => void | Scrolls to an absolute Y offset from the top. |
scrollBy | (delta: number) => void | Scrolls by a relative amount (negative = up, positive = down). |
scrollToTop | () => void | Helper to scroll to offset 0. |
scrollToBottom | () => void | Helper to scroll to the maximum possible offset (contentHeight - viewportHeight). |
getScrollOffset | () => number | Returns the current scroll offset. |
getContentHeight | () => number | Returns the total height of all content items. |
getViewportHeight | () => number | Returns the current height of the visible area. |
getBottomOffset | () => number | Returns the scroll offset when scrolled to the bottom (contentHeight - viewportHeight). |
getItemHeight | (index: number) => number | Returns 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 | () => void | Re-checks viewport dimensions. Must call this on terminal resize. |
remeasureItem | (index: number) => void | Forces a specific child to re-measure. Useful for dynamic content (expand/collapse) that doesn't trigger a full re-render. |
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(<ControlledScrollViewscrollOffset={offset}// ... other props>{children}</ControlledScrollView>);- Unique Keys: Always provide stable, unique
keyprops (strings or numbers) to your children. This allowsScrollViewto accurately track height changes even when items are re-ordered or removed. - Terminal Resizing: Ink components don't automatically know when the terminal window resizes. You need to listen to
process.stdout'sresizeevent and callremeasure()on the ref. - 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:
| Package | Description |
|---|---|
| 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 |
MIT