'Smooth-scroll bug in React useEffect hook only on Chrome / Chromium

i have a bug where a useEffect hook is stopping a scrollIntoView call from completing only on chromium browsers. i imagine there's something i'm not understanding about useEffect. any help's appreciated 🙂

how to reproduce

  1. open this in chrome
  2. navigate to section:1 (click in the nav bar)
  3. navigate to section:5 (click in the nav bar)
  4. the app will start scrolling towards section:5, but get caught on section:2 — the 'smooth scroll' is 'cancelled' for some reason

notes

  • this only happens with 'smooth' scroll behavior
  • only happens on chromium (chrome, edge etc) — firefox and saf are fine

heres a codesandbox link opening it in different browsers shows the issue well

expected behavior (firefox)

expected behavior (firefox)

broken behavior (chrome)

broken behavior (chrome)

styles changing as user scrolls

styles changing as user scrolls

below are the source files aswell as per stack overflow guidelines. tried to make it a so snippet but it didn't wanna work.

App.js

import { useEffect, useRef, useState } from 'react';
import './styles.css';

export default function App() {
  const generateSections = amount => {
    return [...Array(amount).keys()]
       .map(number => number + 1)
       .map(number => {
          return {
            text: `section:${number}`,
            id: `#section-${number}`,
          };
    });
  };

  const sections = generateSections(10);

  const sectionRefs = useRef([]);
  const sectionLinkRefs = useRef([]);
  const navRef = useRef();

  const [activeSectionId, setActiveSectionId] = useState(sections[0].id);

  // 📌 update the active section on scroll (active section is used for styling and     other logic)
  useEffect(() => {
    const changeActiveSection = () => {
      // a small buffer is a bit more intuitive
      const buffer = 50;
      const amountScrolled = window.scrollY + navRef.current.clientHeight + buffer;

      // check what section is scrolled to on the page
      const haveScrolledIntoSection = section => {
        const sectionTop = sectionRefs.current[section.id].offsetTop;
        const sectionBottom = sectionRefs.current[section.id].clientHeight + sectionTop;

        return amountScrolled >= sectionTop && amountScrolled <= sectionBottom;
      };

      // set the active section to be the section scrolled to on the page
      setActiveSectionId(activeSectionId => sections.find(haveScrolledIntoSection)?.id ?? activeSectionId);
    };

    window.addEventListener('scroll', changeActiveSection);
      return () => window.removeEventListener('scroll', changeActiveSection);
    });

    // 📌 center the active section nav link if the active section changes
    useEffect(() => {
      const activeSectionLink = sectionLinkRefs.current[activeSectionId];

      const remainingNavWidth = navRef.current.clientWidth - activeSectionLink.clientWidth;

      navRef.current.scrollLeft = activeSectionLink.offsetLeft - remainingNavWidth / 2;
    }, [activeSectionId]);

    const scrollToSection = sectionId => {
      // 📌📌📌 here is where the bug is! 📌📌📌
      const scrollBehavior = 'smooth';
      // const scrollBehavior = 'auto';

      sectionRefs.current[sectionId].scrollIntoView({ behavior: scrollBehavior });
    };

    return (
      <div>
        <nav ref={navRef}>
          {sections.map(section => {
            const addSectionLinkRef = (ref, sectionId) => {
              if (sectionLinkRefs.current[sectionId] === undefined) sectionLinkRefs.current[sectionId] = ref;
            };

            return (
              <h1
                ref={ref => addSectionLinkRef(ref, section.id)}
                onClick={() => scrollToSection(section.id)}
                className={section.id === activeSectionId ? 'active' : ''}
                key={section.id}
              >
                {section.text}
              </h1>
            );
          })}
        </nav>
        <main>
          {sections.map(section => {
            const addSectionRef = (ref, sectionId) => {
              if (sectionRefs.current[sectionId] === undefined) sectionRefs.current[sectionId] = ref;
            };

          return (
            <section
              ref={ref => addSectionRef(ref, section.id)}
              className={section.id === activeSectionId ? 'active' : ''}
              key={section.id}
            >    
              {section.text}
            </section>
          );
        })}
      </main>
    </div>
  );
}

index.js

import { StrictMode } from "react";
import ReactDOM from "react-dom";

import App from "./App";

const rootElement = document.getElementById("root");
ReactDOM.render(
  <StrictMode>
    <App />
  </StrictMode>,
  rootElement
);

styles.css — largely irrelevant

* {
    padding: 0;
    margin: 0;
    box-sizing: border-box;
}

:root {
    --nav__height: 12.5vh;
    --section__height: calc(100vh - var(--nav__height) - (var(--padding) * 2));
    --padding: 10px;

    --color-primary--normal: #004d40;
    --color-primary--dark: #00251a;
    --color-primary--light: #39796b;
    --color-secondary--normal: #37474f;
    --color-secondary--dark: #102027;
    --color-secondary--light: #62727b;
}

nav {
    height: var(--nav__height);
    width: 100vw;
    background-color: var(--color-primary--normal);
    padding: var(--padding);
    overflow-x: scroll;
    scrollbar-width: none;
    -ms-overflow-style: none;

    display: flex;
    position: fixed;
    gap: var(--padding);
}

nav::-webkit-scrollbar {
    display: none;
}

h1 {
    display: grid;
    height: 100%;
    width: max-content;
    background-color: var(--color-primary--dark);
    place-content: center;
    padding: var(--padding);
    color: var(--color-secondary--normal);
}

.active {
    text-decoration: underline;
    color: white;
}

main {
    padding: var(--padding);
    padding-top: calc(var(--nav__height) + var(--padding));
    background-color: var(--color-secondary--normal);
    display: flex;
    gap: var(--padding);
    flex-direction: column;
}

section {
    height: var(--section__height);
    display: grid;
    place-content: center;
    background-color: var(--color-secondary--light);
    font-size: 2rem;
}


Solution 1:[1]

Wrapping the scrollIntoView call with setTimeout worked for me.

Example:

setTimeout(() => {
    element.scrollIntoView({
        behavior: "smooth",
        block: "center",
        inline: "center"
    });
}, 0);

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 fanczy