How to Use the useScrollOverflow Hook in Astryx for Scroll Detection

The useScrollOverflow hook returns a callback ref and three boolean states (overflowStart, overflowEnd, hasOverflow) to track horizontal scroll boundaries in Astryx components.

The useScrollOverflow hook, available in the facebook/astryx repository, provides a declarative way to detect when scrollable content extends beyond the visible viewport. This utility is essential for building accessible carousels, overflow menus, and scrollable lists that need visual indicators or disabled navigation states. According to the Astryx source code, the hook leverages native ResizeObserver and scroll event listeners to measure container dimensions in real time.

What is useScrollOverflow?

useScrollOverflow is a core React hook exported from @astryxdesign/core that monitors horizontal scroll containers. It reports whether content overflows at the start edge, end edge, or both, enabling dynamic UI adaptations.

The hook returns four key values:

  • scrollRef – A callback ref function to attach to your scrollable container element
  • overflowStart – Boolean indicating content exists before the current scroll position (left in LTR, right in RTL)
  • overflowEnd – Boolean indicating content exists after the current scroll position (right in LTR, left in RTL)
  • hasOverflow – Boolean indicating if total scroll width exceeds the visible container width

Installation and Import

The hook is part of the Astryx core package. Import it directly from the hooks barrel file:

import {useScrollOverflow} from '@astryxdesign/core';

No additional dependencies are required, as the hook utilizes native browser APIs including ResizeObserver.

Implementation Details

The hook operates through three internal mechanisms as implemented in packages/core/src/hooks/useScrollOverflow.ts.

First, it maintains an element reference using useRef (specifically elRef at line 43) to persist the DOM node across renders.

Second, it defines a measure function (lines 51-74) that compares scrollWidth against clientWidth and evaluates scrollLeft to determine boundary detection. This measurement runs on every scroll event and when the container resizes.

Third, it manages cleanup through sharedResizeObserver.ts, which provides observeResize and unobserveResize utilities. The hook automatically removes listeners when the component unmounts, preventing memory leaks.

The hook updates state only when values actually change, optimizing React render cycles by avoiding redundant re-renders.

Practical Code Examples

Basic Horizontal List with Scroll Indicators

Attach the scrollRef to any container with overflowX: auto to detect hidden content:

import {useScrollOverflow} from '@astryxdesign/core';

export function HorizontalList() {
  const {scrollRef, overflowStart, overflowEnd, hasOverflow} = useScrollOverflow();

  return (
    <div>
      <div
        ref={scrollRef}
        style={{
          display: 'flex',
          overflowX: 'auto',
          gap: '8px',
          padding: '4px',
          border: '1px solid #ddd',
        }}
      >
        {[...Array(12)].map((_, i) => (
          <div key={i} style={{minWidth: 120, padding: '8px', background: '#f5f5f5'}}>
            Item {i + 1}
          </div>
        ))}
      </div>
      
      {hasOverflow && (
        <div style={{marginTop: 8, fontSize: 14}}>
          {overflowStart && <span>← More content at start</span>}
          {overflowEnd && <span style={{marginLeft: 12}}>More content at end →</span>}
        </div>
      )}
    </div>
  );
}

The ref callback attaches seamlessly to the div, while the boolean flags drive conditional UI text.

Disable navigation buttons dynamically based on scroll position:

import {useScrollOverflow} from '@astryxdesign/core';
import {ChevronLeftIcon, ChevronRightIcon} from '@astryxdesign/icons';

export function Carousel({children}: {children: React.ReactNode[]}) {
  const {scrollRef, overflowStart, overflowEnd} = useScrollOverflow();

  const scrollBy = (delta: number) => {
    const el = (scrollRef as any).current as HTMLElement | null;
    if (el) el.scrollBy({left: delta, behavior: 'smooth'});
  };

  return (
    <div style={{position: 'relative'}}>
      <button
        disabled={!overflowStart}
        onClick={() => scrollBy(-200)}
        style={{
          position: 'absolute',
          left: 0,
          top: '50%',
          transform: 'translateY(-50%)',
          opacity: overflowStart ? 1 : 0.3,
        }}
      >
        <ChevronLeftIcon />
      </button>

      <div
        ref={scrollRef}
        style={{
          overflowX: 'auto',
          display: 'flex',
          scrollSnapType: 'x mandatory',
          padding: '0 40px',
        }}
      >
        {children.map((child, i) => (
          <div key={i} style={{flex: '0 0 auto', scrollSnapAlign: 'start'}}>
            {child}
          </div>
        ))}
      </div>

      <button
        disabled={!overflowEnd}
        onClick={() => scrollBy(200)}
        style={{
          position: 'absolute',
          right: 0,
          top: '50%',
          transform: 'translateY(-50%)',
          opacity: overflowEnd ? 1 : 0.3,
        }}
      >
        <ChevronRightIcon />
      </button>
    </div>
  );
}

Note: overflowStart returns true when content is hidden at the beginning, so disable the previous button when overflowStart is false.

RTL (Right-to-Left) Layout Support

The hook automatically adapts to document direction by basing calculations on native scrollLeft values:

import {useScrollOverflow} from '@astryxdesign/core';

export function RtlScrollBox() {
  const {scrollRef, overflowStart, overflowEnd} = useScrollOverflow();

  return (
    <div
      dir="rtl"
      ref={scrollRef}
      style={{overflowX: 'auto', whiteSpace: 'nowrap', border: '1px solid #ccc'}}
    >
      <span style={{display: 'inline-block', padding: '20px', margin: '10px', background: '#eee'}}>
        Item A
      </span>
      <span style={{display: 'inline-block', padding: '20px', margin: '10px', background: '#eee'}}>
        Item B
      </span>
      <span style={{display: 'inline-block', padding: '20px', margin: '10px', background: '#eee'}}>
        Item C
      </span>
    </div>
  );
}

In RTL mode, overflowStart refers to the right edge and overflowEnd refers to the left edge, ensuring semantic consistency with the layout direction.

Summary

  • useScrollOverflow provides real-time detection of horizontal scroll boundaries in Astryx applications.
  • The hook returns a callback ref (scrollRef) and three booleans indicating overflow state at the start, end, and overall container.
  • Implementation resides in packages/core/src/hooks/useScrollOverflow.ts and utilizes ResizeObserver for efficient dimension tracking.
  • The hook handles automatic cleanup and only triggers re-renders when state values actually change.
  • RTL layouts are supported natively without additional configuration.

Frequently Asked Questions

What is the difference between overflowStart and hasOverflow?

HasOverflow indicates whether the container requires scrolling at all (content wider than viewport), while overflowStart specifically indicates whether hidden content exists before the current scroll position. You can have hasOverflow: true with overflowStart: false when the user has scrolled to the beginning of the content.

Does useScrollOverflow work with vertical scrolling?

No, the hook is designed specifically for horizontal scroll detection. The implementation in useScrollOverflow.ts measures scrollWidth and clientWidth, comparing horizontal dimensions. For vertical overflow detection, you would need a different utility or custom implementation measuring scrollHeight and clientHeight.

How does the hook impact performance?

The hook uses ResizeObserver rather than polling, and updates React state only when values change from the previous measurement. According to the source code in lines 51-74 of useScrollOverflow.ts, state updates are conditional, preventing unnecessary re-render cycles during scroll events unless the boundary state actually changes.

Can I use useScrollOverflow with custom scrollable components?

Yes, provided the component accepts a ref and forwards it to a scrollable DOM element. Since useScrollOverflow returns a callback ref rather than a mutable ref object, it works with any component using React.forwardRef or standard HTML elements. Ensure the target element has overflow-x: auto or scroll CSS applied for measurements to function correctly.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →