Maintain scroll position in Turbo without data-turbo-permanent

Alright, this will be short and sweet for future me.

Maintaining scroll position is notoriously painful.

Some articles like this have you add data-turbo-permanent:

Why not data-turbo-permanent? Well, in our case we had a sidebar with a highlighted link for the current page, which means link clicks allowed for updating the highlighted current link. There were some workarounds we could have done, but decided not to.

There is also this GitHub issue which has a ton of workarounds:

There are some snippets in there that are pretty close to this. Here’s what I used recently that worked well. Here’s what I came up with that worked for me.

import * as Turbo from '@hotwired/turbo'

if (!window.scrollPositions) {
  window.scrollPositions = {};

function preserveScroll () {
  document.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
    scrollPositions[] = element.scrollTop;

function restoreScroll (event) {
  document.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
    element.scrollTop = scrollPositions[];

  if (!event.detail.newBody) return
  // event.detail.newBody is the body element to be swapped in.
  event.detail.newBody.querySelectorAll("[data-preserve-scroll]").forEach((element) => {
    element.scrollTop = scrollPositions[];

window.addEventListener("turbo:before-cache", preserveScroll)
window.addEventListener("turbo:before-render", restoreScroll)
window.addEventListener("turbo:render", restoreScroll)

There are 2 key things to note. Every element must have a unique ID, and every element must have a data-preserve-scroll on it. Like so:

<nav id="sidebar" data-preserve-scroll>
  <!-- stuff -->

Happy hunting!

EDIT: The one downside to this approach is I’ve noticed a brief flicker in Safari / Chrome. No flicker in FF. Perhaps a Turbo Transition, or using data-turbo-permanent could remove the flicker.

EDIT 2: Fixed the flicker. Article updated.