demo · v130
TOC sidebar sync
The chromestatus motivation cited this scenario by name: "a developer might want to synchronize a table of contents sidebar with the associated content with smooth scrolls, but this is broken by the fact that smoothly scrolling the content would prevent the table of contents from being smoothly scrolled or vice-versa." Two independent scroll containers, two simultaneous scrollIntoView({behavior: "smooth"}) calls, no cancellation. Click any TOC entry to fire both at once.
what changed
Before Chrome 130, scrollIntoView({behavior: "smooth"}) on a scroll container would cancel any other in-flight smooth scroll on another container — even if the two containers had no parent/child relationship. The browser tracked exactly one active smooth-scroll operation per top-level scroll attempt.
That made the canonical docs / books / long-form layout pattern impossible to write without a JS animation loop: tap a TOC entry → the article scrolls to the heading and the TOC scrolls to keep the entry visible. Whichever scrollIntoView call fired second would silently snap the first to its end position.
Chrome 130 aligned with Firefox and Safari: smooth scrolls on containers that are neither descendants nor ancestors of one another run concurrently. Turn on the simulator above to replay the old defensive workaround: wait for the article's smooth scroll to finish, then start the TOC scroll as a second beat.
the code
tocList.addEventListener("click", async (e) => {
const li = e.target.closest("li[data-target]");
if (!li) return;
const heading = body.querySelector(`#${li.dataset.target}`);
if (serial.checked) {
await scrollAndWait(heading, body, { block: "start" });
await scrollAndWait(li, toc, { block: "nearest" });
} else {
// Chrome 130+: these animate together.
heading.scrollIntoView({ behavior: "smooth", block: "start" });
li.scrollIntoView({ behavior: "smooth", block: "nearest" });
}
});