Model or dataset
n8tb1t/use-scroll-position avatar
n8tb1t/use-scroll-position

use-scroll-position tracks window, body and container scroll in React

Track Scroll Like a Pro – React Hook for Smooth UI

629 stars61 forksTypeScriptMIT

At a glance

What is it?
The n8tb1t use-scroll-position package is a small, tree-shakable React hook library for reading scroll position, with three hooks covering the window, the body and overflow containers, and a version 4 that the README describes as a complete rewrite. It is MIT licensed and published to npm under a scoped name.
Who is it for?
Adopt use-scroll-position if you need scroll position in React for a sticky header, an infinite scroll trigger or a scrollspy, and you would rather not attach a scroll listener yourself in every component. Do not adopt it as a virtualisation or scroll restoration library, because it reports positions rather than managing them, and expect to read the legacy documentation if you are still on version 3, since version 4 is a complete rewrite with different hooks.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A v4 rewrite of a small, focused hook library

use-scroll-position is described in the README as a lightweight, tree-shakable React hook library for detecting and tracking scroll position, either of the window or a specific element, designed for performance sensitive use cases. The description is accurate about the size of the thing, which matters for a library that ends up in the dependency list of every component that needs to know where the user is.

The most important line for anyone already using it is the upgrade notice. Version 4.0.0 is a complete rewrite of the library, and older versions are documented separately in a legacy README kept in the repository. That means there is no incremental upgrade path from version 3, and code written against the old API will need rewriting, not adjusting.

The feature list is short: React 19 support, SSR-ready, LLM-friendly docs and native TypeScript. That last item is worth expanding slightly, because the library is written in TypeScript and ships its own types rather than relying on a DefinitelyTyped package, which is the usual arrangement for React utilities.

The package is published to npm as @n8tb1t/use-scroll-position, and the README points to a full documentation site at n8tb1t.github.io/use-scroll-position. The repository is a pnpm workspace with turbo, and there is a packages/ directory that holds the packages including the docs project.

The audience is React developers who need scroll coordinates in component logic, and specifically developers who have grown tired of writing scroll listeners with cleanup by hand.

Three hooks for three different scroll contexts

The library exports three hooks, and the distinction between them is the first thing to understand, because picking the wrong one is the usual source of confusion.

useWindowScrollPosition tracks the global window scroll position. The README's quickstart shows the whole thing in a few lines:

tsx
import { useWindowScrollPosition } from '@n8tb1t/use-scroll-position'

useWindowScrollPosition(({ currPos }) => {
  console.log('Current scroll position:', currPos.y)
})

useBodyScrollPosition does something similar, and the documentation page describes it as similar to window scroll but with a different detection method. The distinction exists because the body element and the window are not the same scroll container in every browser, and the way the position is read differs.

useOverflowScrollPosition tracks scroll inside an overflow container, and it supports a root target plus a child target ref, which is the case that most scroll libraries handle badly.

Each hook can also track an element's position in the viewport rather than the scroll position itself, which is what the ref callback form is for. The hook returns a setter function that you attach as a ref, and the callback then receives the position data for that element:

tsx
const setElementRef = useWindowScrollPosition(({ prevPos, currPos, top, bottom }) => {
  console.log(prevPos, currPos, top, bottom)
})

return <div ref={setElementRef} />

So the same hook serves two roles: called with no arguments it reports the scroll position, and called and used as a ref callback it reports where a particular element sits. That dual use is a small design decision that saves an extra hook in the common case.

The callback contract: previous, current, top and bottom

Every hook takes a single callback, and the callback receives an object with four values: prevPos, currPos, top and bottom. The usage examples in the README all destructure the same four, and the only difference between the window, body and overflow examples is which ref, if any, you attach.

prevPos and currPos are the previous and current scroll positions, each with an x and a y, which is why the quickstart reads currPos.y to get the vertical offset. top and bottom describe the tracked element's position in the viewport rather than the scroll offset itself, and both are useful for the common case of hiding a header when the user scrolls down and showing it again when they scroll up, because top and bottom tell you which way the element is moving relative to the viewport edge.

The important property for performance is that the callback is the only channel for updates. There is no state returned from the hook and no re-render of the component on scroll, which is the usual reason a scroll tracker hurts a page. A component that needs the value can write it to a ref, call an analytics function, or set state behind its own condition, and the library itself does not force a render.

The README does not document the update throttling behaviour, the exact event it listens to, or the browser support matrix, and the documentation site is where those details live. That is a fair thing to check before shipping a hook in a path where every frame matters.

Installing it and the first call

Installation is one command with either package manager, and the README gives both:

bash
npm install @n8tb1t/use-scroll-position
bash
pnpm add @n8tb1t/use-scroll-position

The scoped package name matters here, because the bare name use-scroll-position is not what you install. The README's own examples import from '@n8tb1t/use-scroll-position'.

A first working example for a container is short enough to be worth looking at in full, because it shows the shape of the overflow case:

tsx
const [setRootRef] = useOverflowScrollPosition(({ prevPos, currPos, top, bottom }) => {
  console.log(prevPos, currPos, top, bottom) // root container scroll
})

return (
  <div ref={setRootRef} style={{ height: '100px', width: '300px', overflow: 'auto' }} />
)

Note the return value here is a tuple, and you destructure the first element. That is the difference from the window and body hooks, which return a single function, and it is because the overflow hook needs to hand back more than one ref when you want to track a child element as well.

The tracked element needs a bounded size and an overflow setting, which is why the example sets height, width and overflow inline. A container without a constrained height never scrolls, so the hook has nothing to report.

Two refs for a container: root and target

The overflow hook has a second mode, and it is the one that distinguishes this library from a simple window scroll listener.

When you destructure two elements from the returned tuple, the second ref is a child element inside the scrolling container, and the callback then reports that element's position rather than the container's:

tsx
const [setRootRef, setTargetRef] = useOverflowScrollPosition(({ prevPos, currPos, top, bottom }) => {
  console.log(prevPos, currPos, top, bottom) // target element scroll
})

return (
  <div ref={setRootRef} style={{ height: '100px', width: '300px', overflow: 'auto' }}>
    <div ref={setTargetRef} />
  </div>
)

The practical use is a virtualised list or a long scrollable panel where you need to know where a particular row sits, not just how far the panel has scrolled. The first example in the README comments the root container scroll, the second comments the target element scroll, and the same four values arrive in both cases, which keeps the consuming code identical between the two modes.

This is also where the design shows its purpose. A library that only handled the window would force you to attach a listener to every scrollable element in the application, and to remember to remove it. Handling a root and a target pair means the common cases, a scrolling modal, a virtualised table, a long form pane, are covered by the same API.

What people use it for

The README lists the use cases, and they are a good guide to what the hook is good at.

Sticky headers and navbars is first, described as hiding, revealing or shrinking navigation bars depending on scroll direction. Infinite scrolling is second, detecting when the user reaches the bottom of a container or the window to load more data. Scroll-based animations come next, triggering animations or transitions as elements enter the viewport. Active section highlighting covers the scrollspy effect, updating navigation menus as the user scrolls through sections. Custom parallax effects adjust element positions or backgrounds smoothly based on scroll position, and performance monitoring tracks scroll behaviour for analytics or user experience work.

Six patterns, and they share one requirement: the component needs to know a position in order to change something. The hook supplies the position and leaves the decision to you.

That is a narrower purpose than it might first appear, and it is why the library can be tree-shakable and lightweight. It is not a scroll manager, it is not a virtualised list, it does not restore scroll position on navigation, and it does not smooth or animate scrolling. The use cases listed are all reads. If your requirement is any of the other things, this is the wrong library and the effort will go into building them yourself.

Maintenance: a rewrite in 2025 and a quiet few months since

The release history shows two recent versions and a gap since. The tags are @n8tb1t/use-scroll-position_v3.0.1 from 2025-09-11 and @n8tb1t/use-scroll-position_v4.0.0 from 2025-09-23, and the last push to the repository was on 2026-04-06. The default branch is develop, and the root package.json is marked private, which is normal for a monorepo root that is not itself published.

So the picture is a project that shipped a major rewrite in September 2025 and has had quieter activity since, with the last commit in April 2026. That is a long enough gap that you should check the current state of the package on npm before depending on it, particularly because the v4 rewrite means the API you are evaluating may not be the API most tutorials and blog posts describe.

The tooling tells you how the project is run. The root scripts all delegate to turbo with telemetry disabled, covering build, lint, typecheck and dev, and the dependencies are turbo 2.5.6 and beachball 2.55.1. Beachball is the change file tool that produces the changelog entries and the version bumps, and the configuration in the root package.json points it at the develop branch and the npm registry. That is a deliberate workflow: every change gets a beachball change file, and the release notes are generated rather than written by hand.

The licence is MIT, the primary language is TypeScript, and the documentation site is generated from the packages/docs project inside the workspace.

Editorial conclusion

Adopt use-scroll-position if you need scroll position in React for a sticky header, an infinite scroll trigger or a scrollspy, and you would rather not attach a scroll listener yourself in every component. Do not adopt it as a virtualisation or scroll restoration library, because it reports positions rather than managing them, and expect to read the legacy documentation if you are still on version 3, since version 4 is a complete rewrite with different hooks. Before you upgrade, check which hook you need, read the upgrade notes in the docs site rather than assuming the v3 API carries over, and verify the behaviour on the browsers you support, since the README does not document browser support.

Frequently asked questions

How do I install use-scroll-position?

Install the scoped package with `npm install @n8tb1t/use-scroll-position` or `pnpm add @n8tb1t/use-scroll-position`. The bare package name is not what you install, and every example in the README imports from @n8tb1t/use-scroll-position.

What is the difference between useWindowScrollPosition and useBodyScrollPosition?

Both track the global scroll, and the documentation describes useBodyScrollPosition as similar to window scroll but with a different detection method, because the body element and the window are not the same scroll container in every browser.

How do I track scroll inside an overflow container with use-scroll-position?

Use useOverflowScrollPosition and attach the first element of the returned tuple to the scrolling container, which needs a bounded height and an overflow style. Destructuring a second element gives you a ref for a child element, and the callback then reports that element's position instead of the container's.

What changed in version 4 of use-scroll-position?

The README says version 4.0.0 is a complete rewrite of the library, and older versions are documented separately in a legacy README in the repository. There is no incremental upgrade path from version 3, so code written against the old API needs rewriting rather than adjusting.

Official sources

  1. License: MIT
  2. n8tb1t/use-scroll-position on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/n8tb1t-use-scroll-position.svg)](https://hysenlabs.com/projects/n8tb1t-use-scroll-position)

Was this useful?