Robin van der Vleuten

Scroll a React component into view

React refs give you access to the underlying DOM element when you need a browser API directly. One common example is scrolling an element into view after a button click.

Long pages are normal now, and not everything is visible at first glance. So how do you move someone to content outside the viewport? The browser already has an API for that: Element.scrollIntoView(). It does what the name says, with a few useful options for changing the behavior.

Scroll to element with plain HTML

Before using it in React, start with the plain HTML version.

Suppose we have an article with a long body:

<article>
    <h1 id="title">An interesting article for Latin readers</h1>
    <p>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aliquam sit
        amet luctus neque. Etiam eu quam lacinia, placerat sem ut, interdum
        risus. Quisque ut feugiat mauris. Aenean euismod fermentum facilisis.
        Donec ultricies maximus elit, sit amet convallis urna rhoncus vitae.
        Aliquam bibendum turpis et felis blandit commodo. Donec egestas neque
        non elit laoreet, faucibus tempor ante gravida.
    </p>
    <p>
        Duis in ante turpis. Phasellus dignissim tellus et nunc lacinia
        elementum. Sed venenatis tincidunt justo. Praesent sed purus facilisis,
        porttitor ligula in, mattis velit. Curabitur non pellentesque nunc. Duis
        elit urna, bibendum et purus nec, maximus imperdiet mauris. Cras
        euismod, leo id vehicula vulputate, nibh massa tincidunt justo, sit amet
        fringilla quam orci pellentesque enim.
    </p>
    <p>...</p>
</article>

At the end of the article, we can add a link that points back to the <h1> by id:

<article>
    ...
    <a href="#title"> Back to the top </a>
</article>

When the user clicks the link, the browser jumps back to the title. That is the simplest way to scroll an element into view without JavaScript.

Scroll to element with vanilla JavaScript

To do the same thing with JavaScript, create a button that scrolls back to the top when clicked:

<article>
    ...
    <button onclick="document.getElementById('title').scrollIntoView()">
        Back to the top
    </button>
</article>

The button finds the heading by its title id and tells the browser to scroll it into the viewport.

For many cases, that is enough. If you want a smooth animation, pass an options object:

const titleElement = document.getElementById('title')
titleElement.scrollIntoView({ behavior: 'smooth' })

With behavior set to smooth, the browser scrolls to the element instead of jumping there instantly.

Scroll to a React element

In React, we can still use Element.scrollIntoView(). We just need access to the underlying HTML element.

First, convert the example to a function component:

import React from 'react'

const Article = () => {
    return (
        <article>
            <h1>A React article for Latin readers</h1>
            <p>...</p>
            <p>...</p>
            <button>Back to the top</button>
        </article>
    )
}

We could keep the id, but a ref gives React direct access to the element. Use useRef for that. You can read more about useRef() in the React documentation.

import React, { useRef } from 'react'

const Article = () => {
    const titleRef = useRef()

    return (
        <article>
            <h1 ref={titleRef}>A React article for Latin readers</h1>
            // Rest of the article's content...
            <button>Back to the top</button>
        </article>
    )
}

Next, handle the button click with an onClick event handler. You can read more about event handling in the React documentation.

import React, { useRef } from 'react'

const Article = () => {
    const titleRef = useRef()

    function handleBackClick() {
        // Scroll back to the title element...
    }

    return (
        <article>
            <h1 ref={titleRef}>A React article for Latin readers</h1>
            // Rest of the article's content...
            <button onClick={handleBackClick}>Back to the top</button>
        </article>
    )
}

Inside the event handler, the title element is available through the ref. Then we can call scrollIntoView() just like before.

const titleRef = useRef()

function handleBackClick() {
    titleRef.current.scrollIntoView({ behavior: 'smooth' })
}

useRef() gives the component access to the underlying HTML element, including the DOM APIs on that element.

Scroll to a React component

The same idea works when the element lives inside another React component. Forward the ref to the component's root element, and the parent can still call the DOM API.

import React, { forwardRef, useRef } from 'react'

const Article = forwardRef(({ onBackClick }, ref) => {
    return (
        <article>
            <h1 ref={ref}>A React article for Latin readers</h1>
            // Rest of the article's content...
            <button onClick={onBackClick}>Back to the top</button>
        </article>
    )
})

// ...

const AnotherComponent = () => {
    const articleRef = useRef()

    function handleBackClick() {
        articleRef.current.scrollIntoView({ behavior: 'smooth' })
    }

    return <Article ref={articleRef} onBackClick={handleBackClick} />
}

The forwardRef() call lets another component access the <h1> inside Article by ref. You can read more about forwardRef() in the React documentation.

Bonus: scroll to the first error in a Formik form

For a more realistic use case, imagine a large React form using Formik for submission and validation. Here is a small newsletter signup form:

import React from 'react'
import { Formik } from 'formik'

const SignupForm = () => {
    return (
        <Formik
            initialValues={{ email: '' }}
            validate={(values) => {
                const errors = {}

                if (!values.email) {
                    errors.email = 'Required'
                }

                return errors
            }}
            onSubmit={(values) => {
                // ...
            }}
        >
            {(formik) => (
                <form onSubmit={formik.handleSubmit}>
                    <label htmlFor="email">Email Address</label>
                    <input
                        id="email"
                        name="email"
                        type="email"
                        onChange={formik.handleChange}
                        value={formik.values.email}
                    />
                    {formik.errors.email ? (
                        <div>{formik.errors.email}</div>
                    ) : null}
                    <button type="submit">Submit</button>
                </form>
            )}
        </Formik>
    )
}

When the user submits the empty form, it shows an error for the email field. In this tiny form, that is easy to spot. In a larger form, it helps to scroll to the first field with an error.

Add a small helper component inside the form:

import React, { useEffect } from 'react'
import { useFormikContext } from 'formik'

const ErrorFocus = () => {
    // Get the context for the Formik form this component is rendered into.
    const { isSubmitting, isValidating, errors } = useFormikContext()

    useEffect(() => {
        // Get all keys of the error messages.
        const keys = Object.keys(errors)

        // Whenever there are errors and the form is submitting but finished validating.
        if (keys.length > 0 && isSubmitting && !isValidating) {
            // We grab the first input element that error by its name.
            const errorElement = document.querySelector(
                `input[name="${keys[0]}"]`
            )

            if (errorElement) {
                // When there is an input, scroll this input into view.
                errorElement.scrollIntoView({ behavior: 'smooth' })
            }
        }
    }, [isSubmitting, isValidating, errors])

    // This component does not render anything by itself.
    return null
}

Now add <ErrorFocus> to the Formik form. When validation fails, the user is scrolled to the first input with an error.

import React from 'react'
import { Formik } from 'formik'
import ErrorFocus from './error-focus'

const SignupForm = () => {
    return (
        <Formik
            initialValues={{ email: '' }}
            validate={(values) => {
                const errors = {}

                if (!values.email) {
                    errors.email = 'Required'
                }

                return errors
            }}
            onSubmit={(values) => {
                // ...
            }}
        >
            {(formik) => (
                <form onSubmit={formik.handleSubmit}>
                    <label htmlFor="email">Email Address</label>
                    <input
                        id="email"
                        name="email"
                        type="email"
                        onChange={formik.handleChange}
                        value={formik.values.email}
                    />
                    {formik.errors.email ? (
                        <div>{formik.errors.email}</div>
                    ) : null}
                    <button type="submit">Submit</button>

                    {/* The component itself does not render anything, but needs to be within the Formik context */}
                    <ErrorFocus />
                </form>
            )}
        </Formik>
    )
}

Closing thoughts

Refs are useful when React code needs to call a browser API directly. This post used Element.scrollIntoView(), but the same pattern applies to other element methods too.

For example, elements can be animated through JavaScript with Element.animate(). The MDN documentation covers that API in more detail.