Context
Say we have a collection of elements on the screen with some of them out of view, and want to dynamically (e.g. on a button click) scroll one of those hidden elements into view, we can turn to the Element.scrollIntoView
API.
Below is a contrived example of a horizontally scrollable list of boxes with corresponding buttons for scrolling the box into view:
Horizontal scroll into view
And here's the component code:
export const ScrollIntoViewExample = () => {
const [elRefs, setElRefs] = useState([]);
useEffect(() => {
setElRefs((elRefs) =>
Array(10)
.fill(null)
.map((_, i) => elRefs[i] || createRef())
);
}, []);
return (
<div
style={{
display: 'grid',
placeItems: 'center',
}}
>
<div
style={{
display: 'flex',
width: '360px',
overflowX: 'scroll',
}}
>
{new Array(10).fill(null).map((_, i) => {
return (
<div
key={i}
ref={elRefs[i]}
style={{
whiteSpace: 'nowrap',
margin: '10px',
padding: '20px',
backgroundColor: 'lightgray',
}}
>
Box {i}
</div>
);
})}
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, 1fr)',
gap: '10px',
margin: '20px',
}}
>
{new Array(10).fill(null).map((_, i) => (
<button
key={i}
onClick={()=>
elRefs[i].current.scrollIntoView({
behavior: 'smooth',
block: 'nearest',
inline: 'center',
})
}
>
Scroll box {i} into view
</button>
))}
</div>
</div>
);
};
Let's breakdown this example — the same logic also applies to vertical scrolls, which is probably the more common use case.
Available APIs for scrolling
Before diving into the example, there are a couple of options when it comes to handling scrolling. The APIs available on both Window
and Element
are:
scrollTo
— for scrolling to a specific (x, y) coordinatescrollBy
— for scrolling by a given (x, y) coordinate amount
In addition, Element
has
scrollIntoView
All of these methods apply to both vertical and horizontal scrolling. The scrollIntoView
function used in the interactive example scrolls the container until the element it was called on comes into view. It’s only available on the Element
interface since the window (i.e. the scroll container) itself cannot be scrolled into view.
As for which to use and when, ScrollIntoView
can be called on any scroll-target element, whereas scrollTo
/scrollBy
are called on the container itself.
In short, use ScrollIntoView
to bring an element into view, and scrollBy
and scrollTo
to position the scroll container somewhere specified by the coordinates they accept as arguments.
With an overview of the APIs, let's take a look at how to use scrollIntoView
in React by doing breaking down the example introduced earlier.
ScrollIntoView in react
In practice, the scroll target could be a HTML element or a React component. In either case, we need to grab onto the underlying DOM nodes of the scroll targets so that we can call the scrollIntoView
method on them. For this, we need to reach out for some refs that enable us to call the method on the element or component as needed.
For example, if we wanted to scroll a component into view on page load, it would look like this:
export const ChildComponent = () => {
// create ref for scroll target
const ref =
React.useRef < HTMLElement > null
React.useEffect(() => {
// ensure ref is assigned
if (!ref.current)
throw Error(
'Element ref is not assigned'
)
// fire
ref.current.scrollIntoView()
}, [])
// this component will scroll into view if hidden
// in a scroll container
return (
<HiddenComponentToScrollWrapper
ref={ref}
>
{/* ...children */}
</HiddenComponentToScrollWrapper>
)
}
This would do the job, but doesn't do much beyond bringing the element into view (as stated in the tin). What if we wanted to scroll the element into the middle of the page? The scrollIntoView
accepts an options object that allows some level of customisation. Here's the example above but with the element 'smoothly' scrolled to the middle:
export const ExampleApp = () => {
// create ref for scroll target
const ref = React.useRef < HTMLElement > null
React.useEffect(() => {
// ensure ref is assigned
if (!ref.current)
throw Error('Element ref is not assigned')
// fire
ref.current.scrollIntoView({
// smoothen the scroll
behavior: 'smooth',
// align horizontally
inline: 'center'
})
}, [])
// return
return <ComponentToScroll ref={ref} />
}
These are the same options used in interactive example. When clicking a button to scroll into a given box into view, we can see that the box becomes aligned right in the middle of the scroll container. The exceptions to this are the first (head) and last (tail) elements, and this is because there is no space on either the left or right side to shift the other boxes further to the side. This is one considerable downside of the scrollIntoView
API — there isn't a way to define offsets.
One way to get around this limitation is to add padding to the container to provide enough room to center align the head and tail elements. For more fine-tuned precision, scrollTo
can be called on the scroll container itself. To align any given element right in the middle of the container, we need to calculate and pass the x coordinate of the center point of the element, like so:
scrollContainerRef.current.scrollTo(xCoordCenterOfEl, 0);
The position of any element relative to the viewport is provided by Element.getBoundingClientRect
, which can be used for determining the precise coordinates.
Scroll snapping
It might be desirable to lock, or 'snap' the scroll container to a given element after the user is done scrolling, either with the keyboard or using the slider. This can be done with by adding a few lines of CSS.
To add snapping to the scroll container:
scroll-snap-type: x mandatory;
Then we can define the alginment on the children:
scroll-snap-align: center;
Here's how the interactive example behaves with snapping behaviour:
Horizontal scroll into view with snapping
The snapping is particulary noticeable when scrolling with the arrow keys.
Thoughts
Scrolling elements to view is straightforward with the available APIs, although I would like to see a more extensive set of customisation options in the future, esp. the ability to define offsets.