useScroll
Tracks the scroll position, scroll direction, arrived state (top/bottom/left/right), and scrolling status of any scrollable target — HTMLElement, Document, or Window — as reactive Observable values.
Scroll inside the box to see x, y, arrivedState, and directions update.
Basic — HTMLElement
Section titled “Basic — HTMLElement”import { import useScroll
useScroll, function useRef$<T extends Element = Element>(externalRef?: React.Ref<T> | null): Ref$<T>
Creates an observable element ref. Can be used as a drop-in replacement for
useRef, composed with callback refs, or used with forwardRef.
The element is wrapped with opaqueObject to prevent legendapp/state
from making DOM properties reactive (deep observation).
useRef$ } from "@usels/core";
function function Component(): React.JSX.Element
Component() { const const el$: Ref$<HTMLDivElement>
el$ = useRef$<HTMLDivElement>(externalRef?: React.Ref<HTMLDivElement> | undefined): Ref$<HTMLDivElement>
Creates an observable element ref. Can be used as a drop-in replacement for
useRef, composed with callback refs, or used with forwardRef.
The element is wrapped with opaqueObject to prevent legendapp/state
from making DOM properties reactive (deep observation).
useRef$<interface HTMLDivElement
HTMLDivElement>(); const { const x$: any
x$, const y$: any
y$, const arrivedState$: any
arrivedState$ } = import useScroll
useScroll(const el$: Ref$<HTMLDivElement>
el$);
return ( <React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div React.RefAttributes<HTMLDivElement>.ref?: React.Ref<HTMLDivElement> | undefined
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
ref={const el$: Ref$<HTMLDivElement>
el$} React.HTMLAttributes<HTMLDivElement>.style?: React.CSSProperties | undefined
style={{ StandardShorthandProperties<string | number, string & {}>.overflow?: Property.Overflow | undefined
This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
Syntax: [ visible | hidden | clip | scroll | auto ]{1,2}
Initial value: visible
| Chrome | Firefox | Safari | Edge | IE |
| :----: | :-----: | :----: | :----: | :---: |
| 1 | 1 | 1 | 12 | 4 |
overflow: "auto", StandardLonghandProperties<string | number, string & {}>.height?: Property.Height<string | number> | undefined
This feature is well established and works across many devices and browser versions. It’s been available across browsers since July 2015.
Syntax: auto | <length-percentage [0,∞]> | min-content | max-content | fit-content | fit-content(<length-percentage [0,∞]>) | <calc-size()> | <anchor-size()>
Initial value: auto
| Chrome | Firefox | Safari | Edge | IE |
| :----: | :-----: | :----: | :----: | :---: |
| 1 | 1 | 1 | 12 | 4 |
height: 300 }}> <React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p> scrollX: {const x$: any
x$.any
get()}, scrollY: {const y$: any
y$.any
get()} {const arrivedState$: any
arrivedState$.any
bottom.any
get() && " — reached bottom"} </React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p> </React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div> );}Window scroll
Section titled “Window scroll”Use useWindowScroll for the common case, or pass window directly.
import { useScroll } from "@usels/core";
function Component() { const { y$, arrivedState$, isScrolling$ } = useScroll(window);}Scroll direction
Section titled “Scroll direction”import { useScroll, useRef$ } from "@usels/core";
function Component() { const el$ = useRef$<HTMLDivElement>(); const { directions$ } = useScroll(el$);
// directions$.bottom.get() → true while scrolling down // directions$.top.get() → true while scrolling up}Arrived state with offset
Section titled “Arrived state with offset”Use offset to declare a threshold (in px) before the edge is considered “arrived”.
const { arrivedState$ } = useScroll(el$, { offset: { bottom: 100 }, // bottom=true when within 100px of the end});isScrolling + onStop
Section titled “isScrolling + onStop”const { isScrolling$ } = useScroll(el$, { idle: 300, // ms to wait before isScrolling becomes false (default: 200) onStop: () => { // called when scrolling stops },});Throttle
Section titled “Throttle”const { x$, y$ } = useScroll(el$, { throttle: 50 }); // handler fires at most once per 50msManual re-measure
Section titled “Manual re-measure”const { y$, measure } = useScroll(el$);
// Call measure() to force-sync scroll state without a scroll eventmeasure();Null / SSR-safe target
Section titled “Null / SSR-safe target”Passing null is safe — all observables stay at their initial values and no event listener is registered.
import { useScroll } from "@usels/core";
const target = typeof window !== "undefined" ? document : null;const { y$ } = useScroll(target);Reactive observables, not state. All returned values (x$, y$, isScrolling$, arrivedState$, directions$) are Legend-State Observables. Read them with .get() inside a reactive context (useObserve, etc.) to avoid unnecessary re-renders.
measure() is synchronous. It immediately reads the current scroll values from the DOM and updates all observables. Useful after programmatic scroll operations.
arrivedState initial values. On mount, top and left default to true, and bottom/right default to false. After the first measure() call (triggered automatically on mount), all values are synced with actual DOM state.
Type Declarations
Section titled “Type Declarations”export interface UseScrollOptions { throttle?: number; idle?: number; onScroll?: (e: Event) => void; onStop?: () => void; onError?: (error: unknown) => void; offset?: { left?: number; right?: number; top?: number; bottom?: number; }; behavior?: ScrollBehavior; eventListenerOptions?: MaybeObservable<AddEventListenerOptions>;}export interface ArrivedState { left: boolean; right: boolean; top: boolean; bottom: boolean;}export interface ScrollDirections { left: boolean; right: boolean; top: boolean; bottom: boolean;}export interface UseScrollReturn { x$: Observable<number>; y$: Observable<number>; isScrolling$: Observable<boolean>; arrivedState$: Observable<ArrivedState>; directions$: Observable<ScrollDirections>; measure: () => void;}export declare function useScroll(element: MaybeElement, options?: UseScrollOptions): UseScrollReturn;Source
Section titled “Source”Contributors
Section titled “Contributors”- tigerwest
Changelog
Section titled “Changelog”a7392ab2026-03-06 - feat(core,browser): add sync strategy hooks (tigerwest)