// A devlog by mood.

公開: 更新:

@swup/scroll-plugin更新でスクロール復元が崩れた話

3.x系から4.0系に更新後にブラウザバック時のスクロール位置復元が崩れたので見直したメモ。

@swup/scroll-pluginをアップデートしたら、ブラウザバック時のスクロール位置の復元がうまく動かなくなりました。

追記(2026年9月):この記事はScroll Plugin 4系を試したときの記録です。現在のリポジトリは3系を使用しており、scrollFunctionではなくswup.scrollToの差し替えと、ページの高さを確認してからの位置復元を組み合わせています。以下のコードは当時の対応として読んでください。最初に3系で位置ズレを直した経緯はこちらの記事にあります。

記事一覧から詳細に移動してブラウザバックすると、一覧の元いた位置ではなく、ページ上部に戻るようになってしまいました。

SPAっぽいページ遷移を入れているサイトではよくあるやつですが、今回は@swup/scroll-pluginを4.0系へ更新したタイミングで表面化しました。

使っている構成

このテンプレートでは、ページ遷移とスクロールまわりに以下を使っています。

  • swup
  • @swup/scroll-plugin
  • @swup/js-plugin
  • lenis
  • gsap

Swupでページ遷移を差し替えつつ、Lenisでスムーススクロールを管理しています。
さらにGSAPで遷移アニメーションを入れているので、普通のブラウザ遷移よりスクロール復元のタイミングが少しシビアです。

今回の更新では、@swup/scroll-pluginを3.3系から4.0系へ上げました。
そこで、以前の「swup.scrollToを直接上書きする」やり方を見直すことになりました。

特にブラウザバックでは、

  • 遷移前のスクロール位置を保存する
  • 戻り先のHTMLを差し替える
  • 戻り先の高さが決まる
  • 保存していた位置へ戻す
  • 遷移アニメーションを終える

という順番が崩れると、元の位置に戻れません。

起きたこと

記事一覧の下のほうまでスクロールして記事詳細へ移動したら、ブラウザバックでは一覧の同じ位置に戻ってほしいところです。

でも@swup/scroll-plugin更新後は、スクロール位置が復元されなくなりました。

原因は、Swup Scroll Plugin側のスクロール復元とLenis側のスクロール状態がうまく合っていなかったことでした。
Swup Scroll Pluginには戻り先のスクロール位置が保存されていますが、実際のスクロールはLenisが制御しています。
そのため、Swup側で復元処理が終わっても、Lenis側のスクロール位置が合っていないと、別の位置にいる状態になります。

以前はswup.scrollToを上書きして、そこでLenisのscrollToを呼んでいました。

swup.scrollTo = (offset, animate = true) => {
  const target = Number.isFinite(offset) ? offset : 0;

  if (!lenis) {
    window.scrollTo(0, target);
    return;
  }

  lenis.resize();
  lenis.scrollTo(target, { immediate: !animate, force: true });
};

一旦はこれで動いていたんですが、プラグイン内部の流れに後から差し込む形なので、@swup/scroll-pluginの更新で動かなくなりやすいです。
スクロール開始・終了のフックや、横方向の位置、スクロールコンテナの扱いも自前で合わせる必要がありました。

対処

最終的には、@swup/scroll-plugin v4の流れに合わせて、swup.scrollToを上書きするのをやめました。

代わりに、@swup/scroll-pluginのscrollFunctionを使って、Swup側でスクロール処理が必要になったタイミングだけ、Lenisを呼ぶ形に変更しました。

const scrollWithLenis =
  (lenis: Lenis | null) =>
  (scrollContainer, top, left, animate, start, end) => {
    start();

    if (!lenis) {
      window.scrollTo(left, top);
      end();
      return;
    }

    lenis.resize();

    if (!animate) {
      lenis.scrollTo(top, { immediate: true, force: true });
      end();
      return;
    }

    lenis.scrollTo(top, {
      force: true,
      onComplete: end,
    });
  };

設定側ではこんな感じです。

new SwupScrollPlugin({
  animateScroll: {
    betweenPages: false,
    samePageWithHash: true,
    samePage: true,
  },
  offset: 112,
  scrollFunction: scrollWithLenis(lenis),
});

これで、スクロール位置の保存や復元の判断はSwup Scroll Pluginに任せつつ、実際にスクロールを動かす部分だけLenisに渡せます。

startとendもプラグインから渡されるので、スクロール処理の開始・完了をSwup側に返せるのもよいところです。
自前でscroll:startやscroll:endを呼ぶより、役割がかなり分かりやすくなりました。

まとめ

今回の修正でよかったのは、独自の上書きを減らせたことです。

swup.scrollToを直接差し替えると、最初は手早く直せます。
でも、Swup Scroll Pluginが本来持っている「いつリセットするか」「いつ復元するか」「スクロールコンテナをどう扱うか」まで自分で面倒を見ることになります。

今回はscrollFunctionを使うことで、

  • スクロール位置の保存・復元はプラグインに任せる
  • 実際のスクロール実行だけLenisに任せる
  • 通常遷移では先頭へ戻す
  • ブラウザバックでは元の位置へ戻す

という分担に戻せました。

ライブラリのアップデートで動かなくなったときは、つい動いていた頃の処理を足して直したくなります。
でも、ライブラリ側に用意されている拡張点を使ったほうが、次の更新に強くなりやすいですね。

以上、おわり。