動的に高さが変わる Sticky なサイドメニューの実装サンプル

Material for MkDocs というライブラリの GitHub Pages を見ていて、気になった UI があった。

このサイトは PC で見ると両側にサイドメニューが出るが、この挙動がなかなか面白いのだ。

  1. 画面領域内に収まらない高さになったらスクロールバーが出る (overflow: auto 指定だろう・コレは普通)
  2. 画面をスクロールし始めると、ヘッダの下に埋まらないよう配置される (position: sticky 的な動作)
  3. ページ最下部にスクロールし、フッターが登場すると、画面内に表示されているフッターの高さに合わせて、サイドバーの高さが可変する

3つ目の挙動がなかなか面白い。単なる position: sticky だけの動作ではなさそうだ。

そこでコードを見てみた。

3カラムレイアウトは display: grid などではなく、position: fixed を組み合わせて実装されており、結構大変そうだ。

画面領域内に登場したフッターの高さに合わせて、サイドバーの高さが動的に変わる部分は JavaScript で制御していて、当該要素の height プロパティが設定されていた。

なかなか高度な実装だったので、自分なりに調整してみた。

自分が作ったサンプル

自分が作ったサンプルは以下。

右側のサイドメニューにスクロールバーが出ていると思う。画面をスクロールしていくと、フッタの登場に合わせて、サイドメニューの高さが動的に変わっていく動きを再現できている。

概要は CSS と JS コード中にコメントで書いたが、以降は詳細を説明していく。

実装詳細

CSS は単純な position: sticky のみ使っている。サイドメニューの実装は以下のとおり。

/* サイドメニュー */
.side {
  /* 以下必須 */
  position: sticky;
  top: 80px;  /* ヘッダの高さ分 */
  
  /* 以下任意 */
  padding: 2rem 0;
}

/* スクロールバー表示用に必須 */
.side > .side-scroll-wrapper {
  max-height: 100%;
  overflow-y: auto;
}

.sideposition: sticky を指定している。top にヘッダ分の高さを指定して、基本的には position: fixed 的に動作するようにしている。position: fixed は基準位置が documentElement (body) になってしまうため、できるだけ使いたくない。今回は position: sticky だけで設定している。

CSS はこのくらいで、次は JavaScript。

サイドメニューに指定すべき高さの計算式としては、

サイドメニューの高さ = 画面領域の高さ - ヘッダの高さ - 画面内に表示されているフッタの高さ

と割り出せるが、画面内に表示されているフッタの高さ を割り出すのが若干難しかった。最終的には次のように各値を拾っていった。

「画面内に表示されているフッタの高さ」、および全体の計算結果が負数にならないように、Math.max() を適宜組み合わせ、次のように実装した。

/** サイドメニューの高さを調整する */
function adjustHeight() {
  const pageOffset = window.pageYOffset;  // スクロール量
  const pageHeight = window.innerHeight;  // 描画領域の高さ
  const headerHeight = document.querySelector('.header').offsetHeight;  // ヘッダの高さ
  const footerOffset = document.querySelector('.footer').offsetTop;     // ページ上部からのフッタの登場位置
  
  // 描画領域の高さ - ヘッダの高さ - フッタが見えているピクセル数 = サイドメニューの高さ
  // 高さ 0px 以下にならないようにする
  const height = Math.max(0, pageHeight - headerHeight - Math.max(0, pageOffset + pageHeight - footerOffset));
  
  const side = document.querySelector('.side');
  if(height !== side.offsetHeight) {
    side.style.height = `${height}px`;
  }
}

window.pageYOffset + window.innerHeight - targetElement.offsetTopこの計算式が重要だ。

この height 指定により、position: sticky を指定された要素はいつまでも Sticky を解除するべき位置にならず、position: fixed 的に動き続ける。

あとはページ読み込み時と、スクロール・リサイズ時にこの関数が適用されるようにしておく。

document.addEventListener('DOMContentLoaded', () => {
  adjustHeight();
  document.addEventListener('scroll', adjustHeight);
  window.addEventListener('resize', adjustHeight);
});

以上。position プロパティ周りでちょっと凝ったことしようとすると途端に実装辛くなってくるな…。