'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
- open this in chrome
- navigate to section:1 (click in the nav bar)
- navigate to section:5 (click in the nav bar)
- 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)
broken behavior (chrome)
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 |



