動的に高さが変わる Sticky なサイドメニューの実装サンプル
Material for MkDocs というライブラリの GitHub Pages を見ていて、気になった UI があった。
このサイトは PC で見ると両側にサイドメニューが出るが、この挙動がなかなか面白いのだ。
- 画面領域内に収まらない高さになったらスクロールバーが出る (
overflow: auto
指定だろう・コレは普通) - 画面をスクロールし始めると、ヘッダの下に埋まらないよう配置される (
position: sticky
的な動作) - ページ最下部にスクロールし、フッターが登場すると、画面内に表示されているフッターの高さに合わせて、サイドバーの高さが可変する
3つ目の挙動がなかなか面白い。単なる position: sticky
だけの動作ではなさそうだ。
そこでコードを見てみた。
3カラムレイアウトは display: grid
などではなく、position: fixed
を組み合わせて実装されており、結構大変そうだ。
画面領域内に登場したフッターの高さに合わせて、サイドバーの高さが動的に変わる部分は JavaScript で制御していて、当該要素の height
プロパティが設定されていた。
なかなか高度な実装だったので、自分なりに調整してみた。
自分が作ったサンプル
自分が作ったサンプルは以下。
- デモ : Sticky Fluid Height Side Menu
- コード : frontend-sandboxes/index.html at master · Neos21/frontend-sandboxes
右側のサイドメニューにスクロールバーが出ていると思う。画面をスクロールしていくと、フッタの登場に合わせて、サイドメニューの高さが動的に変わっていく動きを再現できている。
概要は CSS と JS コード中にコメントで書いたが、以降は詳細を説明していく。
実装詳細
CSS は単純な position: sticky
のみ使っている。サイドメニューの実装は以下のとおり。
/* サイドメニュー */
.side {
/* 以下必須 */
position: sticky;
top: 80px; /* ヘッダの高さ分 */
/* 以下任意 */
padding: 2rem 0;
}
/* スクロールバー表示用に必須 */
.side > .side-scroll-wrapper {
max-height: 100%;
overflow-y: auto;
}
.side
に position: sticky
を指定している。top
にヘッダ分の高さを指定して、基本的には position: fixed
的に動作するようにしている。position: fixed
は基準位置が documentElement
(body
) になってしまうため、できるだけ使いたくない。今回は position: sticky
だけで設定している。
CSS はこのくらいで、次は JavaScript。
サイドメニューに指定すべき高さの計算式としては、
サイドメニューの高さ = 画面領域の高さ - ヘッダの高さ - 画面内に表示されているフッタの高さ
と割り出せるが、画面内に表示されているフッタの高さ
を割り出すのが若干難しかった。最終的には次のように各値を拾っていった。
- 画面領域の高さ =
window.innerHeight
- ヘッダの高さ =
document.querySelector('.header').offsetHeight
(ヘッダ要素のoffsetHeight
) - 画面内に表示されているフッタの高さ =
ページのスクロール量 + 画面領域の高さ - フッタ要素の位置
- ページのスクロール量 =
window.pageYOffset
- フッタ要素の位置 =
document.querySelector('.footer').offsetTop
(フッタ要素のoffsetTop
)
- ページのスクロール量 =
「画面内に表示されているフッタの高さ」、および全体の計算結果が負数にならないように、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
プロパティ周りでちょっと凝ったことしようとすると途端に実装辛くなってくるな…。