React scrollIntoView + useRef: Scroll to Any Element
The React Systems Newsletter
SubscribeSign in
React scrollIntoView + useRef: Scroll to Any Element<br>A practical guide to scrolling, refs, fixed headers, newly rendered elements, and the cases where native browser behavior stops being enough.
The React Systems Newsletter<br>Aug 20, 2026
Share
A long form is one of the easiest places to notice bad scrolling behavior.<br>The user clicks Submit. Validation fails somewhere much farther down the page. React renders the error correctly, but the user cannot see it. From their point of view, nothing happened.<br>Thanks for reading! Subscribe for free to receive new posts and support my work.
Subscribe
The fix can be only a few lines:<br>import { useRef } from "react";
export default function Article() {<br>const detailsRef = useRef(null);
function goToDetails() {<br>detailsRef.current?.scrollIntoView({<br>behavior: "smooth",<br>block: "start",<br>});
return (<br><>
Go to details
Details<br>This is the section we wanted to reach.
);<br>}For basic navigation, this is usually all you need.<br>useRef gives you access to the DOM element. scrollIntoView() asks the browser to bring that element into view. The optional chaining is there because detailsRef.current is still null before React mounts the section.<br>There is no library involved, no manual coordinate calculation, and no extra state.<br>The interesting part begins when the page gets more complicated.<br>The options you actually need
The native scrollIntoView() API has a few useful options, but you do not need to memorize all of them before using it.<br>For most React components, these three patterns cover nearly everything.<br>Scroll the element to the top:<br>element.scrollIntoView();Move it smoothly toward the center:<br>element.scrollIntoView({<br>behavior: "smooth",<br>block: "center",<br>});Move only when necessary:<br>element.scrollIntoView({<br>block: "nearest",<br>});The last one is especially useful.<br>Imagine a command menu with twenty results. The user moves through them with the keyboard. If every Arrow Down press centers the active item, the list keeps jumping around even when the next item is already visible.<br>block: "nearest" behaves differently. The browser moves only enough to reveal the target. If the item is already visible, it may not scroll at all.<br>That small difference makes keyboard-controlled lists feel much calmer.<br>There is also an older boolean version:<br>element.scrollIntoView(true);<br>element.scrollIntoView(false);It still works, but I would avoid it in new code. The object form makes the intention much easier to understand later.<br>element.scrollIntoView({<br>block: "end",<br>});One more thing catches people by surprise. scrollIntoView() is not limited to window.<br>If the target sits inside a scrollable sidebar or panel, the browser can scroll that container too. In nested layouts, more than one scrollable ancestor may move so the element becomes visible.<br>Most of the time, this saves you from writing extra code.<br>Fixed headers should be handled in CSS
A sticky navigation bar creates one of the most common scrolling bugs.<br>The element reaches the top of the page exactly as requested, then the header covers it.<br>The first solution people often write looks like this:<br>const HEADER_HEIGHT = 72;
const top =<br>element.getBoundingClientRect().top +<br>window.scrollY -<br>HEADER_HEIGHT;
window.scrollTo({<br>top,<br>behavior: "smooth",<br>});It works until the header changes.<br>Then mobile uses another height. A promo banner appears above it. Someone changes the spacing. The target moves into a nested scrollable container and the calculation no longer describes the real layout.<br>Now the scrolling code owns a magic number that belongs to the CSS.<br>There is already a property for this:<br>.article-section {<br>scroll-margin-top: 5rem;<br>}If the header height is stored in a variable, even better:<br>:root {<br>--header-height: 5rem;
.article-section {<br>scroll-margin-top: var(--header-height);<br>}Your JavaScript can stay simple:<br>sectionRef.current?.scrollIntoView({<br>behavior: "smooth",<br>block: "start",<br>});scroll-margin-top does not move the element in the normal layout. It only changes how much space the browser leaves when positioning that element during scrolling.<br>For a fixed or sticky header, this should be the first solution you try.<br>Scrolling to something React just created
This is where many otherwise correct examples break.<br>Suppose you add a new row and want to scroll to it immediately:<br>function addRow() {<br>setRows((current) => [...current, createRow()]);
lastRowRef.current?.scrollIntoView({<br>behavior: "smooth",<br>});<br>}The code looks reasonable, but the new row may not exist yet.<br>Calling setRows() schedules an update. React still has to render the new list and commit the result to the DOM.<br>The next line runs before that process is guaranteed to be finished.<br>At that moment, lastRowRef.current can still point to the previous row.<br>Scroll after React commits the update
An effect is the simplest solution when scrolling should happen because state...