2026-01-11
Building a Universal "Scroll-To" Hook in React & Next.js
On a landing page, navigation usually involves smooth scrolling to sections. However, once you add multiple pages (like a Blog or FAQ), a simple eleme...
On a landing page, navigation usually involves smooth scrolling to sections. However, once you add multiple pages (like a Blog or FAQ), a simple element.scrollIntoView() isn't enough. You need a way to navigate back to the home page and then scroll to the section.
The Math Behind the Scroll
To get the exact position for window.scrollTo, we use a specific formula. Think of it like finding a specific page in a book:
-
getBoundingClientRect().top: How far the element is from the current top of your screen. -
window.scrollY: How far you have already scrolled down the page. -
offset: Space for your sticky header so it doesn't cover the section title.
The Formula: Target = Viewport Top + Current Scroll - Offset
The Implementation: useScrollTo
This hook handles three scenarios:
-
External Links: Redirects normally.
-
Same-page Scroll: Scrolls smoothly and updates the URL hash.
-
Cross-page Scroll: Navigates to the correct page first, then scrolls after the page loads.
JavaScript
;
/**
* A universal hook for smooth scrolling that handles
* both same-page and cross-page navigation.
*/
How to Use It
Simply call the hook in your component and pass the ID of the section you want to reach.
JavaScript
const scrollTo = ;
<Button onClick=
Why This Approach is Better:
-
URL Synchronicity: It uses
window.history.pushState. If the user refreshes or shares the link, the#idstays in the URL, allowing the browser to find the spot again. -
The "Header Gap" Fix: Most landing pages have a sticky navbar. By passing an
offset, you prevent the navbar from overlapping your section headings. -
Next.js Integration: Using
router.push().then()ensures that we only attempt to scroll after Next.js has finished the route transition.
Pro-Tip: Why the setTimeout?
In the cross-page scenario, we use a small delay (100ms–150ms). This is because Next.js needs a split second to render the DOM elements on the new page before document.getElementById can actually find them.