Scrolling Elements Into View in React

February 13, 2022

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

Box 0
Box 1
Box 2
Box 3
Box 4
Box 5
Box 6
Box 7
Box 8
Box 9

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) coordinate
  • scrollBy — 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

Box 0
Box 1
Box 2
Box 3
Box 4
Box 5
Box 6
Box 7
Box 8
Box 9

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.