useScrollState React hook in TypeScript
v1.0.0Added in 1.0.0
A React scroll position hook: offset, progress and hide-on-scroll state for the window or a container, as motion values without re-renders.
Scroll the feed in the phone frame: the top bar slides away with your finger as you scroll down and comes back the moment you scroll up, then settles fully shown or hidden when you stop. The progress line under it fills as you read, the large title fades into the bar, and a back-to-top button appears once you pass the threshold. The readout shows direction, scrolled and hidden flipping without a render per frame.
useScrollState is a Pro React hook in the React hooks and utilities category, written in TypeScript for React 19. It installs as source with npx wingo-ui@latest add use-scroll-state, together with useMediaQuery and Motion presets and the npm package motion.
Add to a project
Requirements: React 19, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add use-scroll-stateuseScrollState is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import useScrollState from @/hooks/use-scroll-state.
import { motion, useTransform } from "motion/react"
import { useScrollState } from "@/hooks/use-scroll-state"
import { cn } from "@/lib/utils"
export function AppHeader() {
const { hideProgress, scrolled } = useScrollState({ hideOnScroll: true, hideDistance: 56 })
const y = useTransform(hideProgress, [0, 1], [0, -56])
return (
<motion.header
style={{ y }}
className={cn(
"sticky top-0 z-sticky flex h-14 items-center bg-background/80 px-4 backdrop-blur-xl",
scrolled && "border-b border-foreground/10",
)}
>
<h1 className="text-[17px] font-semibold">Listings in New York</h1>
</motion.header>
)
}API
hideOnScrolldefaultfalsebooleanuseScrollState(options?): track hideProgress and hidden for a bar that slides away while you scroll down. Off by default, so hooks that only read the offset do no extra work.
hideDistancedefault56numberPixels of scrolling that take a bar from shown to hidden, usually the bar's own height, so it moves 1:1 with the content.
hideOffsetdefault0numberThe bar never hides above this offset, e.g. while a page header is still on screen. It can always come back.
snapdefaulttruebooleanAfter scrolling stops, a half-hidden bar settles to shown or hidden on the snappy spring.
revealAtEnddefaulttruebooleanBring the bar back at the very end of the content, where nothing is left to uncover.
scrolledThresholddefault1numberscrolled turns true past this many pixels: 1 for a header hairline, 400 for a back-to-top button.
targetHTMLElement | Window | RefObject<HTMLElement | null> | nullWhat to watch. Default: the nearest ScrollContainerProvider, else the window. A ref is read after mount; pass the element itself when it can change.
disableddefaultfalsebooleanStops listening; the values keep their last state.
returns{ offset: MotionValue<number>; progress: MotionValue<number>; hideProgress: MotionValue<number>; direction: "up" | "down" | null; scrolled: boolean; hidden: boolean; atEnd: boolean; scrollTo(top, { smooth? }): void }offset (px, negative during an iOS bounce), progress (0 to 1) and hideProgress (0 shown to 1 hidden, following the finger) are motion values: feed them to useTransform and style. direction, scrolled, hidden and atEnd re-render only when they flip. scrollTo is smooth unless the user prefers reduced motion; under reduced motion hideProgress only ever jumps between 0 and 1.
Parts
ScrollContainerProvider
componentDeclares the element that scrolls for everything inside it (an app shell's content area, a demo box, a phone frame). Keep the element in state through a callback ref so the children update once it mounts. Props: ({ value: HTMLElement | null; children }) => JSX.Element.
useScrollContainer
hookThe nearest provided scroll container, or null for the window. Scroll spies use it as their IntersectionObserver root. Signature: () => HTMLElement | null.
findScrollParent
functionThe nearest vertically scrolling ancestor of an element, or null for the window. Signature: (element) => HTMLElement | null.
scrollToOffset
functionScrolls an element or the window to an offset outside React, instant under reduced motion. Signature: (target, top, smooth?) => void.
ScrollContainerProviderProps
type{ value: HTMLElement | null; children?: ReactNode }.
ScrollTarget
typeWhat target accepts: HTMLElement | Window | RefObject<HTMLElement | null> | null | undefined.
ScrollDirection
type"up" | "down", the last scroll direction.
UseScrollStateOptions
typeThe options object: target, scrolledThreshold, hideOnScroll, hideDistance, hideOffset, revealAtEnd, snap and disabled.
ScrollState
typeThe return value: offset, progress and hideProgress motion values, the direction, scrolled, hidden and atEnd flags, and scrollTo.
Examples
Reading progress
A bar that fills as you read an article in its own scroll container, through the target option.
Bottom actions that step aside
hideOnScroll slides the action bar down while you scroll the list and brings it back on the way up.
Dependencies
Files
- hooks/use-scroll-state.ts
FAQ
Is the useScrollState hook free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install useScrollState?
Run npx wingo-ui@latest add use-scroll-state. It also adds the Wingo UI items it builds on (useMediaQuery and Motion presets) and the npm package motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/use-scroll-state.
See also
Used in
More React hooks and utilities
Articles using this component
- React Collapsing Header: iOS Large Title and Photo Hero
Mobile UI in React8 min read







