本文へスキップ

現在地ハイライト

縦に並ぶセクションをスクロールすると、サイドの目次の現在地が自動でハイライトされる。

プレビュー

スクロール

操作枠内をスクロールすると動きます。

使いどころ

見出しが縦に連続する長めのドキュメント(仕様書、規約、レポート)で、読者が『いま自分がどのセクションを読んでいるか』を目次側でも把握できるようにしたいときに向く。サイドの目次が72px幅に収まる短いラベルを想定しており、見出しの数が4〜6個程度で各セクションがある程度の分量を持つ場合に効果的。逆に、セクションが1〜2行で終わる短い一覧(FAQなど)ではハイライトが目まぐるしく切り替わるだけで意味を持たない。またこのデモは.spy-content自体がoverflow-y:autoの独立したスクロールコンテナになっており、ページ全体のスクロールではなくコンテナ内スクロールを前提にしている。実際のページでウィンドウ全体をスクロールさせる構成にするなら、IntersectionObserverのrootをnull(ビューポート)に変える必要がある。

仕組み

現在地の検出はIntersectionObserverで行い、root: content(独自スクロールコンテナ)、threshold: 0.4を指定することで『セクションの40%が見えたら現在地とみなす』判定にしている。コールバックはentries.forEach内でentry.isIntersectingがtrueのものだけ処理し、対応するリンクにis-activeを付け、他の全リンクからis-activeを外す形。目次クリック時はpreventDefaultでhref="#…"のデフォルト遷移を止め、content.scrollTo({top: target.offsetTop, behavior:'smooth'})でコンテナ内だけをスムーズスクロールさせている(documentではなくcontentへのscrollToである点が通常のページ内リンクジャンプと異なる)。.spy-section:last-childにはmin-height:200pxを別途与えており、これは最後のセクションが短いとビューポート内で40%を満たせず、一度もハイライトされないまま終わる問題を避けるための調整。

実装の注意

threshold: 0.4は『40%以上見えている間ずっとtrue』という判定なので、隣接するセクションの境界付近では両方が同時に0.4を超えることがある。IntersectionObserverのコールバックは複数entryをまとめて配列で返すため、forEach内で両方is-activeが付き、配列の後ろ側(通常は下のセクション)が最終的に上書きして残る。単一のセクションだけを厳密にハイライトしたいなら、entries全体からintersectionRatioが最大のものだけを選ぶロジックに直す必要がある。もう一つ、isIntersectingがfalseになったときにis-activeを外す処理がないため、ハイライトの消灯は『別のセクションがtrueになって上書きされる』ことに依存している。スクロール末端に達したあと、どのセクションも0.4を割り込むと、直前のis-activeがそのまま残り続ける(意図した挙動かどうかは要件次第で判断が必要)。

アクセシビリティ

現状はクラスの見た目だけでハイライトしており、aria-currentがどのリンクにも付いていない。スクリーンリーダー利用者は色の変化を知覚できないため、目次のどの項目が現在地かを音声だけでは判別できない。実装するときはIntersectionObserverのコールバック内でis-activeの切り替えと同時にaria-current="location"(または"true")を対象リンクへ付け、非アクティブなリンクからは属性ごと外す処理を加える必要がある。もう一つの弱点はキーボード利用者向けの導線で、.spy-tocがDOM上.spy-contentより先に来るため、Tabキーで本文へ到達する前に必ず目次の全リンクを順番にたどることになり、スキップするためのリンク(『本文へスキップ』のようなショートカット)が用意されていない。見出し数が多い実ページに応用する際は、目次の先頭にスキップリンクを1つ足すか、目次を独立したランドマークとして案内し、ランドマーク間ジャンプで本文へ直接飛べることを利用者に伝える工夫が要る。

調整して使う

調整できる値はthreshold(現在0.4。低くすると早めに切り替わり、高くすると遅れて切り替わる)、目次の位置(左サイドが定番だが右サイドや上部固定タブに変えることもできる)、そしてスクロール方式(コンテナ内scrollかページ全体scrollか)。ハイライトを単純な文字色の変化ではなく、tabs-underlineのような下線が縦に動くインジケータへ発展させる表現もできる。見出しの階層(h2/h3のネスト)がある文書では、目次側もインデント付きの階層リストにし、IntersectionObserverの監視対象をh2だけでなくh3にも広げる拡張が考えられるが、その場合はthresholdの調整だけでなく、どの階層を優先してハイライトするかのロジックを別途組む必要がある。

コード(コピーして使えます)

<div class="spy-root">
  <nav class="spy-toc">
    <a class="spy-link is-active" href="#spy-s1" data-target="spy-s1">はじめに</a>
    <a class="spy-link" href="#spy-s2" data-target="spy-s2">概要</a>
    <a class="spy-link" href="#spy-s3" data-target="spy-s3">仕様</a>
    <a class="spy-link" href="#spy-s4" data-target="spy-s4">まとめ</a>
  </nav>
  <div class="spy-content">
    <section id="spy-s1" class="spy-section">
      <h2>はじめに</h2>
      <p>目次の現在地がハイライトされます。</p>
    </section>
    <section id="spy-s2" class="spy-section">
      <h2>概要</h2>
      <p>IntersectionObserverで実装しています。</p>
    </section>
    <section id="spy-s3" class="spy-section">
      <h2>仕様</h2>
      <p>外部ライブラリ不要のVanilla JSです。</p>
    </section>
    <section id="spy-s4" class="spy-section">
      <h2>まとめ</h2>
      <p>スクロールに連動して目次が追従します。</p>
    </section>
  </div>
</div>

タグ

同じカテゴリ「ナビ・リスト操作」のほかのパーツ

関連パーツ