Skip to content

ByteLandTechnology/ink-scroll-view

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.

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.

Ref Methods (ScrollViewRef)

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.

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.

πŸ”— Related Packages

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

License

MIT

About

A ScrollView component for Ink CLI applications

Topics

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published