4. スクロール制御とルート遷移
サイトナビゲーションを常時表示すると,長い週次記事で本文に利用できる表示領域が狭くなります。そのため,Real Terms では下方向のスクロール時にサイトナビゲーションを非表示にし,上方向のスクロール時とページ先頭付近で再表示します。この挙動は,VitePress で構築した Significant Bit と共通です。
表示状態を表すクラス
表示位置は CSS で制御します。.eg-site-nav は position: fixed で配置し,transform の変化に 0.3 秒のトランジションを設定します。
.eg-site-nav.scrolled-down {
transform: translateY(-100%);
pointer-events: none;
}
.eg-site-nav.scrolled-up,
.eg-site-nav.is-open {
transform: translateY(0);
}.scrolled-down はサイトナビゲーションを上方向へ移動し,pointer-events: none によって非表示の要素がポインター操作を受け取ることを防ぎます。メニュー展開中は .is-open が translateY(0) を適用するため,全画面パネルを表示したままヘッダーだけが画面外へ移動する状態は生じません。
利用者が prefers-reduced-motion: reduce を指定している場合は,トランジションを無効にします。
setupSiteNavScrollReveal
スクロール方向の判定は,app.js の setupSiteNavScrollReveal() が担います。この関数は,siteChrome の ready フックから一度だけ呼び出します。
function sync() {
var nav = document.querySelector(".eg-site-nav");
if (!nav) {
return;
}
var y = window.scrollY;
if (nav.classList.contains("is-open") || y <= nearTopPx(nav)) {
nav.classList.remove("scrolled-down");
nav.classList.add("scrolled-up");
lastY = y;
return;
}
if (y > lastY) {
nav.classList.add("scrolled-down");
nav.classList.remove("scrolled-up");
} else if (y < lastY) {
nav.classList.remove("scrolled-down");
nav.classList.add("scrolled-up");
}
lastY = y;
}判定条件は次のとおりです。
- 先頭付近
nearTopPx()はmax(8, nav.offsetHeight)を返します。高さが 64px の場合,ページ先頭から約 64px の範囲ではサイトナビゲーションを表示します。 - 下方向
現在位置yが直前の位置lastYより大きい場合は,.scrolled-downを付けます。 - 上方向
yがlastYより小さい場合は,.scrolled-upを付けます。位置が変化しない場合は,現在の表示状態を維持します。 - メニュー展開時
.is-openが付いている場合は,スクロール位置にかかわらず表示します。setSiteNavOpen()もメニューを展開するときに.scrolled-downを外し,.scrolled-upを付けます。
scroll イベントのリスナーには { passive: true } を指定し,この処理がスクロールを抑止しないことをブラウザーへ明示します。
ルート遷移後の再表示
Docsify はハッシュルーターを使用し,記事を開くと URL のハッシュが #/2026/weekly/CW36.md のように変化します。$docsify.auto2top も有効ですが,ルート遷移後の表示状態を確実に初期化するには,サイトナビゲーション側の処理も必要です。第2章で説明した tabindex の追加と組み合わせることで,ホームを表示したときに先頭見出しへのスクロールによってサイトナビゲーションが非表示になる現象を防ぎます。
ルートが変わるたびに,次の処理を実行します。
function pinTop() {
revealNav();
if (isHome() && !hasHeadingTarget() && window.scrollY !== 0) {
window.scrollTo(0, 0);
}
lastY = window.scrollY;
sync();
}
function syncAfterRoute() {
pinTop();
routeTimers.forEach(clearTimeout);
routeTimers = [setTimeout(pinTop, 50)];
}syncAfterRoute() は,次の二つの契機で呼び出されます。
hashchange
ブラウザーのハッシュが変化した時点で呼び出します。siteChromeのdoneEach
Docsify が各ルートの本文を描画し終えた時点で呼び出します。描画後にスクロール位置が変化する場合に備え,50ms 後にもpinTop()を実行します。
isHome() は,現在のハッシュが記事を指しているかどうかを currentReportPath() によって判定します。ホームであり,?id= 形式の見出しターゲットがなく,現在位置が先頭でない場合に限って scrollTo(0, 0) を実行します。見出しアンカーがある場合は,その見出しへの移動を優先します。
ハッシュルーティングと通常 URL の分離
サイトナビゲーションのリンクには通常のパスを使用し,Docsify のハッシュは引き継ぎません。Home,Significant Bit,About のいずれかを選択すると,Real Terms の単一ページアプリケーションから別のページへ遷移します。
Real Terms 内の記事移動は,サイドバーと本文リンクが担います。これらのリンクは #/… 形式のハッシュルーティングを使用します。サイトナビゲーションは現在のサイトを示し,サイドバーはそのサイト内の現在の記事を示します。公式 Navbar を採用しないことで,サイト間の移動とドキュメント内の移動を明確に分離しています。