Tech

ページトップを判定したいだけなのに

最終更新:2026.03.02

課題

よくある「トップページに戻る」ボタンが画面固定されている場合について、自分がページトップにいれば非表示(非活性)・いなければ表示(活性)するようにしたい。

問題

もっとも容易に考えられる条件はY方向のスクロール値 window.scrollY が「ゼロか」「ゼロでないか」の時である。

つまりスクロールイベントが発生するたびに window.scrollY === 0 かを判定すればよいのだが、 window.addEventListener(’scroll’, … ) はご存知の通り(?)悪名高く、DebounceやThrottleといったテクニックがないと不要なスクロール処理が過剰発生することで知られている。

ただ、これらの「処理を間引く」ためのテクニックを使った場合、間引いているために少し反応が遅れて見えてしまったり細かいUI上の違和感が現れることもあり、適切でない場面もある印象。

そのため具体的な要素(ヘッダーが隠れた時)などの条件であればIntersection Observerで監視してあげるのがスマートでよいのだが、「ページトップか」を判断するにはそのためだけに見えない1pxの要素をページの一番上に置いて〜などのハックを使うことになるため少し煩わしい。

タイトル通りやりたいのはただ自分がページトップにいるかどうかを判定したいだけなのに……。

解決

アクセスした時点では活性状態にしたくないため、あらかじめ「トップページに戻る」ボタンには inert 属性を付けておく。

<a href="#" id="pagetop" inert>トップページに戻る</a>

※ hidden (非表示)と違いinert (非活性)属性だけでは非表示にはならないため、別途CSSにて非表示するための記載が必要。

/* CSSアニメーションを付けたい場合は別途transitionなどを使ってください */
#pagetop[inert] { visibility: hidden; }

この前提をもってJavascript上で #pagetop の表示・非表示= inert の真・偽を切り替える。

function pagetopToggle() {
  let ticking = false;
  const pagetop = document.querySelector('#pagetop');

  window.addEventListener('scroll', function() {
    if (!ticking) {
      window.requestAnimationFrame(function() { // 画面のフレームレートに応じて処理を間引く
        const isTop = window.scrollY === 0; // scrollTop: 0ならtrue

        if ( pagetop.inert !== isTop ) { // isTopがinertの有無と一致しなくなった時
          pagetop.inert = isTop; // ページトップ(true)かトップでないか(false)を一致させる
        }
        ticking = false;
      });
      ticking = true;
    }
  }, { passive: true }); // 既存のスクロール処理を邪魔しない=preventDefaultを使用しないことを明示
}

もちろん window.scrollY はスクロールのたびに取得されるが、 inert の付け外し=画面上の変更処理については「ページトップでなくなった」「ページトップに戻った」の2回のみ働くため、パフォーマンス上はIntersection Observerによる監視と同等の軽量さが得られる(はず)。

また、念のため requestAnimationFrame ないし ticking によって画面のフレームレートごとに最低限の「間引き」も行っている。おそらくこれが一番お手軽かも。

※別解:animation-timeline

動くか試せていないのとパフォーマンス的な観点では未知数だが、CSSの animation-timeline が使えるようになればこれだけで済むらしい(すばらしすぎる……)。

ただ、現状ではFirefoxなどにpolyfillが必要なためいずれはJSでCSSをゴニョゴニョする時代も終わるのかもしれないという希望だけ……。

https://caniuse.com/mdn-css_properties_animation-timeline

body {
  animation-timeline: scroll();
  animation-range: 0 1px; 
}

#pagetop {
  visibility: hidden; /* content-visibility: none; */
  animation: toggle-pagetop linear both;
}

@keyframes toggle-pagetop {
  to {
    visibility: visible; /* content-visibility: visible; */
  }
}