Let's say you're creating a reusable button component in React and want to use it to navigate to a particular section or component of your website from various other components.
One approach we're familiar with is using <Link /> and <Route />. However, these are primarily used for navigation between different routes. What if you want to scroll to a specific section within the same page instead?
That's where this blog comes in! You'll learn how to scroll between separate components in React.
First, let's create a Button component.
export default function Button() { return ( <button>Scroll to View</button> ); } Next, let's create the section/component we want to scroll to.
export default function CatGallery() { return ( <div> <h2>Hey there, Hooman!</h2> <img src="https://images.unsplash.com/photo-1592194996308-7b43878e84a6?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=NHwxMjA3fDA%3D" alt="orange cat" /> </div> ); } Now, we have two separate components and our task is to scroll to CatGallery when the user clicks the button component.
But how do we do that?
As we know, in React, every component is like an isolated island. The Button component only knows about itself and CatGallery only knows about itself.
Finding the Common Ancestor
Remember how Harry and Voldemort shared a common ancestor in Harry Potter and the Deathly Hallows? Well, we need to find a common ancestor here too!
To connect two separate components, we need to move up the component tree and find their lowest common parent.
Let's create our App.jsx:
import {useRef} from "react"; import Hero from './Hero'; import CatGallery from './CatGallery'; export default function App() { const galleryRef = useRef(null); return ( <div> <Hero /> // This is where you place your Button component <CatGallery /> // This is the target section </div> ); } export default App; Centralizing Control
Now comes the interesting part!
We'll create the useRef hook and the scroll function inside this common parent component.
Since App renders both Hero (which contains the Button) and CatGallery, it acts as the middleman connecting the trigger (the button click) to the target (the CatGallery section).
This way, both components can communicate through their common parent without needing to know about each other's internal implementation.
So, how exactly does this work?
useRef creates a persistent object ({ current: null }) that survives across component re-renders. Unlike state, changing its current property does not trigger a re-render.
Attaching the ref to the DOM:
When you pass this ref down to CatGallery and attach it to an actual HTML element (such as <div ref={ref}>), React automatically assigns that DOM element to galleryRef.current once the component mounts.
export default function CatGallery({ ref }) { return ( <div ref={ref}> <h2>Hey there, Hooman!</h2> <img src="https://images.unsplash.com/photo-1592194996308-7b43878e84a6?q=80&w=687&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=NHwxMjA3fDA%3D" alt="orange cat" /> </div> ); } Scrolling to the target:
Calling galleryRef.current.scrollIntoView() tells the browser to scroll the viewport until the referenced DOM element becomes visible. By passing { behavior: "smooth" }, we make the scrolling animation smooth rather than instant.
import { useRef } from "react"; import Hero from "./Hero"; import CatGallery from "./CatGallery"; export default function App() { const galleryRef = useRef(null); const handleScrollToExplore = () => { galleryRef.current?.scrollIntoView({ behavior: "smooth" }); }; return ( <div> <Hero onClick={handleScrollToExplore} /> {/* This is where you place your Button component */} <CatGallery ref={galleryRef} /> {/* This is the target section */} </div> ); } export default App; The Trigger (Prop Drilling + Callback Functions)
In JavaScript, functions are first-class citizens. This means they can be passed around just like strings, numbers, or other values.
App passes the handleScrollToExplore function down to Hero through props, and Hero passes it further down to Button.
export default function Hero({ onClick }) { return ( <main> <h1>Hey there, nice to have you!</h1> <Button goToExplore={onClick} /> </main> ); } The Button component doesn't need to know where it's scrolling or how the scrolling works. It simply needs to call the function it receives (goToExplore) whenever a user clicks the button.
export default function Button({ goToExplore }) { return ( <button onClick={goToExplore}> Scroll to View </button> ); } This is the beauty of callback functions! They allow one component to trigger an action without needing to know the internal logic behind it.
Here is the final result


