Skip to content

Restore scroll position when navigating back to a list - #7143

Open
tassilogravura wants to merge 2 commits into
stashapp:developfrom
tassilogravura:fix/back-navigation-scroll-restoration
Open

tassilogravura wants to merge 2 commits into
stashapp:developfrom
tassilogravura:fix/back-navigation-scroll-restoration

Conversation

@tassilogravura

@tassilogravura tassilogravura commented Aug 4, 2026

Copy link
Copy Markdown

Description

React Router v5 has no scroll restoration, so the app relied on the browser's native restoration. That doesn't work for the list views: at the point the history entry is popped, the list content hasn't been fetched and rendered yet, so the document is one viewport tall and the position is clamped to the top. Nothing scrolls back once the content arrives, leaving the user at the top of the list.

Record the scroll position for each history entry and re-apply it on POP until the page is able to scroll there, holding it briefly while the list settles. Positions are kept in session storage so they survive a page reload.

Coming back to a list that was showing a page other than the first re-parses the filter from the query string, which looks like a page change to useScrollToTopOnPageChange and scrolled over the restored position, so that hook now defers to an in-progress restoration.

Related Issue

Closes #7142

Testing

manual testing in my own browser

Screenshots

Checklist

  • I have read and understood the Contributing document.
  • I have read and understood the AI Usage Policy document.
  • I have made corresponding changes to the documentation (if applicable).

AI Usage Disclosure

  • I have used AI tools to assist with this pull request, and I have disclosed the tools and how I used them below.
  • Claude Opus 5

Additional Context

React Router v5 has no scroll restoration, so the app relied on the
browser's native restoration. That doesn't work for the list views: at
the point the history entry is popped, the list content hasn't been
fetched and rendered yet, so the document is one viewport tall and the
position is clamped to the top. Nothing scrolls back once the content
arrives, leaving the user at the top of the list.

Record the scroll position for each history entry and re-apply it on
POP until the page is able to scroll there, holding it briefly while
the list settles. Positions are kept in session storage so they survive
a page reload.

Coming back to a list that was showing a page other than the first
re-parses the filter from the query string, which looks like a page
change to useScrollToTopOnPageChange and scrolled over the restored
position, so that hook now defers to an in-progress restoration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBjwuhWgCvRbAvcYed3mfv
@tassilogravura
tassilogravura force-pushed the fix/back-navigation-scroll-restoration branch from 74a1992 to dbea5e9 Compare August 6, 2026 10:57
@hyper440

Copy link
Copy Markdown
Contributor

One concern: scrollToWhenReady relies on fixed 3s/750ms timers, which could fail on slow mobile queries. Could restoration run once the list finishes loading, with a ResizeObserver if the layout still needs to settle?

Restoring the scroll position gave up after three seconds and held the
restored position for a fixed 750ms afterwards. Both numbers were
guesses about how long a list takes to arrive, and on a slow query - a
big library, an unindexed sort, a phone on a weak connection - the
three seconds run out while the page is still one viewport tall. The
restoration is abandoned and the viewer is left at the top, which is
the whole thing this was meant to fix.

The clock now only runs when nothing is loading. Lists report that they
are waiting on their content, and a restoration in progress waits for
them however long they take; the three seconds are left as a fallback
for pages that report nothing, and a thirty second cap keeps a page
that never loads from suppressing the automatic scrolling forever.
PagedList is the one place every list view renders through, so the one
call there covers all of them.

Re-applying is driven by the document changing height rather than by a
callback on every frame, so the position goes back as soon as the page
is tall enough to take it. A slow interval alongside it watches the
deadlines, which no resize reports, and covers the case of custom CSS
pinning the height of the elements being watched. The first attempt is
still made synchronously so that content that was already cached is
restored before the page is painted.

The hold afterwards now starts when the content is in rather than when
the position first became reachable, which is what the 750ms was
approximating - a quarter of a second is enough once it is measured
from the right moment. Pressing something now abandons the restoration
as scrolling already did, since with the longer cap a press on the
pagination has to be able to get out of the way.

Checked in a browser against the real hook: a load that takes eight
seconds now lands at the right place where it previously landed at the
top, cached content still restores before the first paint, a competing
scroll to the top during the hold is overridden, a wheel event
abandons the restoration, and a page that never loads gives up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114kctDcLF4XiCKpaQt8Vz9
@tassilogravura

Copy link
Copy Markdown
Author

One concern: scrollToWhenReady relies on fixed 3s/750ms timers, which could fail on slow mobile queries. Could restoration run once the list finishes loading, with a ResizeObserver if the layout still needs to settle?

Good catch, the 3s is real. With an 8s query it gives up and you land at the
top, which is the exact bug this fixes.

Deadline is conditional now: lists report while loading (one call in PagedList,
which all of them go through) and the clock only runs when nothing is loading.
3s stays as a fallback, 30s hard cap.

Took the ResizeObserver suggestion too, so re-applying is driven by the document
growing instead of a rAF loop.

Kept the settle hold but cut it to 250ms, measured from when loading finishes.
It isn't waiting for layout, it holds the position against
useScrollToTopOnPageChange, which scrolls without changing geometry, so no
resize fires for it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Scroll position is lost when navigating back to a list

2 participants