Skip to content

Conversation

@jacob314
Copy link
Contributor

@jacob314 jacob314 commented Sep 11, 2025

This resolves #765

The primary goal was to introduce a robust scrolling mechanism directly into the native Ink <Box> component, inspired by the overflow scroll model on the web.

Overview

  • Scrollable Overflow: A <Box> with overflow: 'scroll' should not expand beyond its flexbox-defined height. Instead, its content should be scrollable.
  • Virtual Scrollbar: A visual, non-interactive scrollbar should be rendered on the right-hand side of the box to indicate the scroll position and the amount of overflow. Ink users are able to theme the scrollbar to align with the UI of their application.
  • Initial Scroll Position: The component should accept a prop to define whether the initial scroll position is at the top or the bottom of the content. This is helpful for CLI Agents where often content should be scrolled to the bottom by default.
  • Programmatic Control: An API is available programmatically get scroll information (like scrollHeight) and set the scroll position.
  • Horizontal Scrolling: Support for horizontal scrolling with a horizontal scrollbar.

Non goals for the initial implementation

  • Supporting lazy list rendering to increase efficiency for very large scrollable lists.
  • Implementing interactive scrollbars within Ink. These could be adopted later or layered on top of Ink with Ink potentially surfacing the apis to simulate mouse events but with Ink not having an opinion about how the mouse events should be detected.

2. Implementation Strategy

The implementation involved the following steps:

Step 1: Extending the Styling System

  • File: src/styles.ts
  • Changes:
    • Modified the Styles type definition to add 'scroll' as a valid value for the overflow, overflowX, and overflowY properties.
    • Added new style properties to the Styles type for configuring the scrollbar's appearance and behavior (scrollTop, scrollLeft, initialScrollPosition, scrollbarThumbCharacter, etc.).
    • Introduced a new function, applyOverflowStyles, to set the YGOverflowScroll property on the underlying Yoga layout node when overflow: 'scroll' is specified. This is the key to enabling Yoga's native overflow measurement capabilities.

Step 2: Updating the Box Component

  • File: src/components/Box.tsx
  • Changes:
    • Updated the component's Props type to include the new scroll-related properties (scrollLeft), making them available to developers.

Step 3: Modifying the Internal DOM Structure

  • File: src/dom.ts
  • Changes:
    • Extended the DOMElement type to include internal fields for storing calculated scroll state (internal_scrollTop, internal_scrollHeight, internal_clientHeight). This allows the renderer to access this information without needing to recalculate it.
    • Added getScrollWidth to calculate the total width of the content and other measurement metrics required for users to implement their own keyboard or mouse based scroll on top of the Ink library without Ink having to commit to an API for that.

Step 4: Implementing the Core Scrolling Logic

  • File: src/render-node-to-output.ts
  • Changes:
    • This file saw the most significant changes. The renderNodeToOutput function was heavily modified to:
      1. Detect Scrollable Boxes: Check if a <Box> has overflowX: 'scroll' or overflowY: 'scroll'.
      2. Calculate Scroll Dimensions: If a box is scrollable, calculate its clientHeight/clientWidth (the visible area) and scrollHeight/scrollWidth (the total size of its children).
      3. Manage Scroll Position: Determine the correct scrollTop and scrollLeft based on the props, ensuring it stays within valid bounds.
      4. Render the Scrollbar: Calculate the scrollbar thumb's size and position and render both the track and the thumb for both vertical and horizontal scrollbars.
      5. Apply Content Scrolling: Apply a negative vertical and/or horizontal offset to the children of the scrollable box to simulate scrolling.
      6. Clip Overflowing Content: Set up a clipping region to ensure that content outside the visible area (including the area reserved for the scrollbar) is not rendered.

Step 5: Verification

  • Files:
    • examples/scroll/scroll.tsx
    • examples/scroll/index.ts
  • Changes:
    • Added a scroll example to visually test and verify both vertical and horizontal scrolling functionality.
    • The example includes a <Box> with a flex model driven height and width, overflowing content, and interactive controls to switch between scrolling and not scrolling on each axis.
@jacob314 jacob314 force-pushed the scroll branch 9 times, most recently from 0fc8312 to aad59b8 Compare September 16, 2025 18:55
@jacob314 jacob314 force-pushed the scroll branch 2 times, most recently from 03d2cd5 to f7a0845 Compare October 14, 2025 22:48
@jacob314 jacob314 marked this pull request as ready for review October 14, 2025 22:49
@jacob314
Copy link
Contributor Author

jacob314 commented Oct 14, 2025

We have this and a couple follow on changes working in a prototype branch of Gemini CLI.
This functionality is helping us significantly reduce flicker by making content scrollable that we wouldn't have been able to previously.

Sneak.preview.of.scrollbars.webm

To try out:
npx https://github.com/google-gemini/gemini-cli#jacob314/scroll

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

1 participant