Search across all documentation pages
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
import { useState, useEffect, useCallback } from "react";
interface UseScrollToTopOptions {
/** Scroll distance (px) before the button appears. Default: 300 */
threshold?: number;
/** Use smooth scrolling. Default: true */
smooth?: boolean;
}
interface UseScrollToTopReturn {
/** Whether the page has scrolled past the threshold */
isVisible: boolean;
/** Call this to scroll to the top */
scrollToTop: () => void;
/** Current scroll position */
scrollY: number;
}
function useScrollToTop(
options: UseScrollToTopOptions = {}
): UseScrollToTopReturn {
const { threshold = 300, smooth = true } = options;
const [isVisible, setIsVisible] = useState(false);
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
const y = window.scrollY;
setScrollY(y);
setIsVisible(y > threshold);
};
// Check initial position
handleScroll();
window.addEventListener("scroll", handleScroll, { passive: true });
return () => window.removeEventListener("scroll", handleScroll);
}, [threshold]);
const scrollToTop = useCallback(() => {
window.scrollTo({
top: 0,
behavior: smooth ? "smooth" : "instant",
});
}, [smooth]);
return { isVisible, scrollToTop, scrollY };
}When to reach for this: You have a long page and want to give users a quick way to return to the top, with the button only appearing after they have scrolled down a meaningful distance.
"use client";
function BackToTopButton() {
const { isVisible, scrollToTop } = useScrollToTop({
threshold: 400,
smooth: true,
});
return (
<button
onClick={scrollToTop}
aria-label="Scroll to top"
style={{
position: "fixed",
bottom: 24,
right: 24,
width: 48,
height: 48,
borderRadius: "50%",
border: "none",
background: "#111",
color: "#fff",
fontSize: 20,
cursor: "pointer",
opacity: isVisible ? 1 : 0,
transform: isVisible ? "translateY(0)" : "translateY(16px)",
transition: "opacity 0.3s, transform 0.3s",
pointerEvents: isVisible ? "auto" : "none",
}}
>
↑
</button>
);
}
function LongPage() {
return (
<div>
<h1>Article Title</h1>
{Array.from({ length: 50 }, (_, i) => (
<p key={i}>Paragraph {i + 1} of content...</p>
))}
<BackToTopButton />
</div>
);
}What this demonstrates:
pointerEvents: "none" prevents the invisible button from blocking clicksaria-label ensures screen reader accessibilityscroll event listener (with { passive: true } for performance) tracks window.scrollY.isVisible flips to true when scrollY exceeds the threshold, giving the consuming component a reactive boolean for rendering.scrollToTop calls window.scrollTo with behavior: "smooth" for a native smooth scroll animation.passive: true option tells the browser the handler will not call preventDefault, allowing scroll performance optimizations.| Parameter | Type | Default | Description |
|---|---|---|---|
options.threshold | number | 300 | Pixels scrolled before isVisible is true |
options.smooth | boolean | true | Whether to use smooth scroll behavior |
| Return | Type | Description |
|---|---|---|
isVisible | boolean | Whether scroll position exceeds threshold |
scrollToTop | () => void | Function to scroll to top |
scrollY | number | Current scroll Y position |
Throttled scroll handler: For pages with heavy rendering, wrap the scroll handler with useThrottledCallback to reduce state updates:
const handleScroll = useThrottledCallback(() => {
setScrollY(window.scrollY);
setIsVisible(window.scrollY > threshold);
}, 100);Scroll to element: Extend to scroll to any element ref instead of the top:
const scrollToElement = useCallback((ref: React.RefObject<HTMLElement>) => {
ref.current?.scrollIntoView({ behavior: smooth ? "smooth" : "instant" });
}, [smooth]);requestAnimationFrame.window is not available during server-side rendering. Fix: The useEffect only runs on the client, so the hook is SSR-safe as written. Default state (isVisible: false) is correct for SSR.behavior: "smooth". Fix: The page still scrolls instantly, which is an acceptable fallback.bottom value or add z-index to layer correctly.| Package | Hook/Component | Notes |
|---|---|---|
react-scroll | animateScroll.scrollToTop() | Full scroll library with link components |
usehooks-ts | useScrollPosition | Tracks position but no scroll-to function |
ahooks | useScroll | Returns full scroll state for any element |
framer-motion | useScroll | Animation-focused scroll tracking |
| Native CSS | scroll-behavior: smooth | CSS-only, no button logic |
isVisible is true when window.scrollY exceeds the threshold value.false when the user scrolls back above the threshold.The passive flag tells the browser the handler will never call preventDefault(). This allows the browser to optimize scroll performance by not waiting for the handler to finish before scrolling.
window.scrollTo({ behavior: "smooth" }) triggers a native smooth scroll animation.behavior property and scroll instantly.This handles the case where the user refreshes the page while already scrolled down. Without the initial call, isVisible would remain false until the next scroll event.
Wrap the handler with useThrottledCallback:
const handleScroll = useThrottledCallback(() => {
setScrollY(window.scrollY);
setIsVisible(window.scrollY > threshold);
}, 100);bottom CSS value to push the button above the nav bar.z-index to ensure proper layering.If opacity: 0 is set without pointerEvents: "none", the button still receives click events. The working example sets pointerEvents: isVisible ? "auto" : "none" to prevent this.
Yes. The useEffect only runs on the client, and the default state values (isVisible: false, scrollY: 0) are correct for server-rendered output. No typeof window guard is needed outside the effect.
Use the named interfaces directly:
interface Props {
scrollOptions: UseScrollToTopOptions;
}
function MyComponent({ scrollOptions }: Props) {
const result: UseScrollToTopReturn = useScrollToTop(scrollOptions);
}Add a scrollToElement function using scrollIntoView:
const scrollToElement = useCallback(
(ref: React.RefObject<HTMLElement>) => {
ref.current?.scrollIntoView({
behavior: smooth ? "smooth" : "instant",
});
},
[smooth]
);Reviewed by Chris St. John·Last updated Jul 19, 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥