Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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;
}
like image 752
Solomon Broadbent Avatar asked Nov 16 '25 09:11

Solomon Broadbent


1 Answers

Wrapping the scrollIntoView call with setTimeout worked for me.

Example:

setTimeout(() => {
    element.scrollIntoView({
        behavior: "smooth",
        block: "center",
        inline: "center"
    });
}, 0);
like image 113
fanczy Avatar answered Nov 18 '25 10:11

fanczy



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!