Summary
On iOS Safari, when a user flick-scrolls upward inside a long discussion and PostStream.loadPostsIfNeeded() triggers stream.loadPrevious() to prepend older posts, the viewport jumps — often all the way to the top of the discussion. The faster the flick, the more severe the jump; a very slow, deliberate scroll works correctly. Desktop browsers are unaffected.
Affected versions
Steps to reproduce
- Open a long discussion on an iPhone (Safari). The user should already have read through it so the "end" is the target post.
- Flick-scroll upward briskly to page back through older posts.
- Keep scrolling until the top of the currently-loaded window is about to leave the viewport and
loadPrevious() fires.
Observed: the viewport jumps sharply — sometimes to the beginning of the discussion — as the prepended batch lands.
Expected: the viewport stays anchored on whichever posts the user was looking at (same behavior as desktop).
Why this only appears post-#4561
#4561 ("perf: frontend performance improvements") changed the viewport meta from
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1">
to
<meta name="viewport" content="width=device-width, initial-scale=1">
which is a correct accessibility fix — users can now pinch-zoom on mobile. On beta.8 (with maximum-scale=1, minimum-scale=1), iOS Safari runs a simpler scroll pipeline in which programmatic scrollTop writes during momentum scroll tend to be honored; once user-zoom is enabled, iOS's zoom-and-scroll coordination layer kicks in and programmatic scrollTop writes during in-flight momentum are silently dropped. This is the same well-known WebKit behavior that has bitten many virtualized-list libraries; the viewport change in #4561 merely unmasked it in Flarum.
The accessibility win from #4561 should stay; the PostStream jump needs a client-side fix so momentum-scroll pagination works regardless of zoom configuration.
Root cause
PostStreamState.loadPage() wraps the prepend redraw in anchorScroll():
// framework/core/js/src/forum/states/PostStreamState.ts
const redraw = () => {
if (start < this.visibleStart || end > this.visibleEnd) return;
const anchorIndex = backwards ? this.visibleEnd - 1 : this.visibleStart;
anchorScroll(`.PostStream-item[data-index=\"${anchorIndex}\"]`, m.redraw.sync);
};
anchorScroll() itself is:
// framework/core/js/src/common/utils/anchorScroll.js
export default function anchorScroll(element, callback) {
const $window = $(window);
const relativeScroll = $(element).offset().top - $window.scrollTop();
callback();
$window.scrollTop($(element).offset().top - relativeScroll);
}
On iOS Safari during momentum (inertial) scrolling with user-zoom enabled, programmatic writes to window.scrollTop are silently ignored — the in-flight momentum animation wins. As a result:
- The batch of placeholders is prepended to
.PostStream.
- The final
$window.scrollTop(...) call is dropped.
- The momentum animation continues from the old absolute scroll offset, which now points far earlier in the discussion because the content above the viewport has grown by ~20 items — producing the observed jump.
Related reports with the same pattern:
CSS scroll-anchoring (overflow-anchor) would normally be a safety net, but WebKit only shipped it in Safari 26 (WebKit tracker #171099, caniuse). A grep of the Flarum JS tree for overflow-anchor returns no matches, so there is no fallback today either.
Suggested fix
Route the restore through window.scrollTo(x, y) instead of jQuery's $window.scrollTop(y). window.scrollTo goes through a different WebKit code path that DOES halt the in-flight momentum and apply the new position, while scrollTop writes are silently dropped:
- $window.scrollTop($(element).offset().top - relativeScroll);
+ window.scrollTo(window.scrollX, $(element).offset().top - relativeScroll);
This is a one-line surgical change with no UX or layout side effects on any platform — on non-iOS browsers it's equivalent behavior. PR to follow.
Environment
- Flarum
2.x-dev (staging).
- Affected device: iPhone, mobile Safari.
- Desktop Chrome, Firefox, and Safari are all fine.
- Custom extensions installed on the reporting forum do not touch the post-stream scroll path and are not involved; reproduced with the symptom caused by core behavior alone.
Summary
On iOS Safari, when a user flick-scrolls upward inside a long discussion and
PostStream.loadPostsIfNeeded()triggersstream.loadPrevious()to prepend older posts, the viewport jumps — often all the way to the top of the discussion. The faster the flick, the more severe the jump; a very slow, deliberate scroll works correctly. Desktop browsers are unaffected.Affected versions
2.x-dev(current).v2.0.0-beta.8— see "Why this only appears post-[2.x] perf: frontend performance improvements — preconnect hints, fetchpriority, FOUC fix, viewport #4561" below.Steps to reproduce
loadPrevious()fires.Observed: the viewport jumps sharply — sometimes to the beginning of the discussion — as the prepended batch lands.
Expected: the viewport stays anchored on whichever posts the user was looking at (same behavior as desktop).
Why this only appears post-#4561
#4561 ("perf: frontend performance improvements") changed the viewport meta from
to
which is a correct accessibility fix — users can now pinch-zoom on mobile. On beta.8 (with
maximum-scale=1, minimum-scale=1), iOS Safari runs a simpler scroll pipeline in which programmaticscrollTopwrites during momentum scroll tend to be honored; once user-zoom is enabled, iOS's zoom-and-scroll coordination layer kicks in and programmaticscrollTopwrites during in-flight momentum are silently dropped. This is the same well-known WebKit behavior that has bitten many virtualized-list libraries; the viewport change in #4561 merely unmasked it in Flarum.The accessibility win from #4561 should stay; the PostStream jump needs a client-side fix so momentum-scroll pagination works regardless of zoom configuration.
Root cause
PostStreamState.loadPage()wraps the prepend redraw inanchorScroll():anchorScroll()itself is:On iOS Safari during momentum (inertial) scrolling with user-zoom enabled, programmatic writes to
window.scrollTopare silently ignored — the in-flight momentum animation wins. As a result:.PostStream.$window.scrollTop(...)call is dropped.Related reports with the same pattern:
CSS scroll-anchoring (
overflow-anchor) would normally be a safety net, but WebKit only shipped it in Safari 26 (WebKit tracker #171099, caniuse). A grep of the Flarum JS tree foroverflow-anchorreturns no matches, so there is no fallback today either.Suggested fix
Route the restore through
window.scrollTo(x, y)instead of jQuery's$window.scrollTop(y).window.scrollTogoes through a different WebKit code path that DOES halt the in-flight momentum and apply the new position, whilescrollTopwrites are silently dropped:This is a one-line surgical change with no UX or layout side effects on any platform — on non-iOS browsers it's equivalent behavior. PR to follow.
Environment
2.x-dev(staging).