483

useScroll

Hook that allows you to control scroll a element

sensorslowtest coverage

Meet reactuse — a hooks library you will love

reactuse3 min read

reactuse is a collection of essential React hooks for everyday development. Fully typed, tree-shakeable and built around a consistent API — whether you need debounce, local storage, media queries or device sensors, there is probably a hook for it.

Every hook follows the same shape, so once you learn one you already know the rest. Options go in, a small object comes out, and the ref is always there when you need to attach to a DOM node.

Take useScroll. It gives you a reactive snapshot of the scroll position, the direction of travel, and the arrived edges — top, bottom, left and right — without any manual math.

Wire the callback straight to the DOM and you never pay for a rerender. Update styles imperatively as the user scrolls, exactly like you would with a mouse-driven spotlight.

The arrived state flips the moment a user reaches an edge. No off-by-one threshold bugs, no scrollHeight juggling scattered across effects — the hook already did the work.

Directions reveal intent. Is the user scrolling up or down right now? That single bit of information powers hiding headers, lazy loading and scroll-triggered animations.

Because the value is a snapshot, you opt into rerenders only when you actually want them. Read it imperatively, or watch it — the choice stays with you.

You have reached the end of the article. The progress bar above just hit one hundred percent — try scrolling back up to watch it rewind.

import { useScroll } from '@siberiacancode/reactuse';
import { ClockIcon, UserIcon } from 'lucide-react';
import { useRef } from 'react';

const PARAGRAPHS = [
  'reactuse is a collection of essential React hooks for everyday development. Fully typed, tree-shakeable and built around a consistent API — whether you need debounce, local storage, media queries or device sensors, there is probably a hook for it.',
  'Every hook follows the same shape, so once you learn one you already know the rest. Options go in, a small object comes out, and the ref is always there when you need to attach to a DOM node.',
  'Take useScroll. It gives you a reactive snapshot of the scroll position, the direction of travel, and the arrived edges — top, bottom, left and right — without any manual math.',
  'Wire the callback straight to the DOM and you never pay for a rerender. Update styles imperatively as the user scrolls, exactly like you would with a mouse-driven spotlight.',
  'The arrived state flips the moment a user reaches an edge. No off-by-one threshold bugs, no scrollHeight juggling scattered across effects — the hook already did the work.',
  'Directions reveal intent. Is the user scrolling up or down right now? That single bit of information powers hiding headers, lazy loading and scroll-triggered animations.',
  'Because the value is a snapshot, you opt into rerenders only when you actually want them. Read it imperatively, or watch it — the choice stays with you.',
  'You have reached the end of the article. The progress bar above just hit one hundred percent — try scrolling back up to watch it rewind.'
];

const Demo = () => {
  const barRef = useRef<HTMLDivElement>(null);

  const scroll = useScroll<HTMLDivElement>((params) => {
    const el = scroll.ref.current;
    if (!el || !barRef.current) return;

    const max = el.scrollHeight - el.clientHeight;
    const progress = max > 0 ? Math.min(100, (params.y / max) * 100) : 0;
    barRef.current.style.width = `${progress}%`;
  });

  return (
    <section className='flex min-w-xs flex-col gap-4 md:min-w-md'>
      <div className='relative overflow-hidden rounded-xl'>
        <div className='bg-muted absolute top-0 right-0 left-0 z-10 h-1'>
          <div
            ref={barRef}
            className='bg-primary h-full w-0 transition-[width] duration-100 ease-out'
          />
        </div>

        <div ref={scroll.ref} className='no-scrollbar flex h-96 flex-col gap-5 overflow-y-auto p-5'>
          <header className='flex flex-col gap-2'>
            <h1 className='text-foreground text-2xl leading-tight font-semibold'>
              Meet reactuse — a hooks library you will love
            </h1>
            <div className='text-muted-foreground flex items-center gap-3 text-sm'>
              <span className='flex items-center gap-1.5'>
                <UserIcon className='size-3.5' />
                reactuse
              </span>
              <span className='flex items-center gap-1.5'>
                <ClockIcon className='size-3.5' />3 min read
              </span>
            </div>
          </header>

          <article className='flex flex-col gap-4'>
            {PARAGRAPHS.map((text, index) => (
              <p key={index} className='text-foreground text-base leading-relaxed'>
                {text}
              </p>
            ))}
          </article>
        </div>
      </div>
    </section>
  );
};

export default Demo;

Installation

pnpm add @siberiacancode/reactuse

Usage

const { scrolling, scrollIntoView, scrollTo} = useScroll(ref, options);
// or
const { scrolling, scrollIntoView, scrollTo} = useScroll(ref, () => console.log('callback'));
// or
const { ref, scrolling, scrollIntoView, scrollTo} = useScroll(options);
// or
const { ref, scrolling, scrollIntoView, scrollTo} = useScroll(() => console.log('callback'));

Type Declarations

import type { HookTarget } from '@/utils/helpers';

import type { StateRef } from '../useRefState/useRefState';

export interface UseScrollOptions {
  /** Offset arrived states by x pixels. */
  offset?: {
    left?: number;
    right?: number;
    top?: number;
    bottom?: number;
  };

  /** The on scroll callback */
  onScroll?: (params: UseScrollCallbackParams, event: Event) => void;

  /** The on end scroll callback */
  onStop?: (event: Event) => void;
}

export interface UseScrollCallbackParams {
  /** State of scroll arrived */
  arrived: {
    left: boolean;
    right: boolean;
    top: boolean;
    bottom: boolean;
  };
  /** State of scroll direction */
  directions: {
    left: boolean;
    right: boolean;
    top: boolean;
    bottom: boolean;
  };
  /** The element x position */
  x: number;
  /** The element y position */
  y: number;
}

export interface ScrollIntoViewParams {
  behavior?: ScrollBehavior;
  block?: ScrollLogicalPosition;
  inline?: ScrollLogicalPosition;
}

export interface ScrollToParams {
  behavior?: ScrollBehavior;
  x: number;
  y: number;
}

export interface UseScrollReturn {
  /** The latest scroll value snapshot */
  snapshot: UseScrollCallbackParams;
  /** Function to scroll element into view */
  scrollIntoView: (params?: ScrollIntoViewParams) => void;
  /** Function to scroll element to a specific position */
  scrollTo: (params?: ScrollToParams) => void;
  /** Function to enable subscriptions and rerender on next updates */
  watch: () => UseScrollCallbackParams;
}

export interface UseScroll {
  (
    target?: HookTarget,
    callback?: (params: UseScrollCallbackParams, event: Event) => void
  ): UseScrollReturn;

  (target: HookTarget, options?: UseScrollOptions): UseScrollReturn;

  <Target extends Element>(
    callback?: (params: UseScrollCallbackParams, event: Event) => void,
    target?: never
  ): UseScrollReturn & { ref: StateRef<Target> };

  <Target extends Element>(
    options?: UseScrollOptions,
    target?: never
  ): UseScrollReturn & {
    ref: StateRef<Target>;
  };
}

API

Parameters

NameTypeDefaultNote
options.behaviorScrollBehaviorautoThe behavior of scrolling
options.offset.leftnumber0The left offset for arrived states
options.offset.rightnumber0The right offset for arrived states
options.offset.topnumber0The top offset for arrived states
options.offset.bottomnumber0The bottom offset for arrived states
options.onScroll(params: UseScrollCallbackParams, event: Event) => void-The callback function to be invoked on scroll
options.onStop(event: Event) => void-The callback function to be invoked on scroll end

Returns

UseScrollReturn

Parameters

NameTypeDefaultNote
callback(params: UseScrollCallbackParams, event: Event) => void-The callback function to be invoked on scroll

Returns

UseScrollReturn

Parameters

NameTypeDefaultNote
targetTargetwindowThe target element to scroll
options.behaviorScrollBehaviorautoThe behavior of scrolling
options.offset.leftnumber0The left offset for arrived states
options.offset.rightnumber0The right offset for arrived states
options.offset.topnumber0The top offset for arrived states
options.offset.bottomnumber0The bottom offset for arrived states
options.onScroll(params: UseScrollCallbackParams, event: Event) => void-The callback function to be invoked on scroll
options.onStop(event: Event) => void-The callback function to be invoked on scroll end

Returns

UseScrollReturn & { ref: StateRef<Target> }

Parameters

NameTypeDefaultNote
targetTarget-The target element to scroll
callback(params: UseScrollCallbackParams, event: Event) => void-The callback function to be invoked on scroll

Returns

UseScrollReturn & { ref: StateRef<Target> }

Contributors

ddebabinEEvgen41kk

Last updated on