Scrollspy ナビゲーションを作成する

スクロールスパイ ナビゲーションは、現在ビューポートに表示されているページのセクションに対応するナビゲーション リンクを自動的にハイライト表示する一般的な UI パターンです。これにより、ユーザーは長いドキュメントをスクロールする際に、自分がどこにいるかを明確に把握できます。

このパターンは、次の 2 つのシナリオでよく使用されます。

  • 単一ページのウェブサイト: メインのサイト ナビゲーションが、個別の URL ではなく、単一ページの異なるセクションにリンクしているウェブサイト。
  • 長文コンテンツ: 目次やサイドバー ナビゲーションで、記事、ガイド、ドキュメントのユーザーの進行状況を追跡します。

導入方法

従来、スクロールスパイ パターンには JavaScript と IntersectionObserver API が必要でした。最新の CSS では、scroll-target-group プロパティと :target-current 疑似クラスを使用して、これをネイティブに実現できます。

  1. ナビゲーション リンクでフラグメント識別子が使用されていることを確認する: ナビゲーションは、コンテンツ セクションの id と一致するフラグメント識別子(例: <a href="#section-1">)を使用して、同じページ上のセクションを指すアンカーリンク(<a>)で構成されている必要があります。
  2. スクロール ターゲットのグループ化を有効にする: ナビゲーション リンクのコンテナに scroll-target-group: auto プロパティを適用します。これにより、ブラウザはこれらのリンクを、それぞれのターゲットを追跡するマーカーとして扱うようになります。
  3. アクティブなリンクのスタイルを設定する: ナビゲーション リンクで :target-current 疑似クラスを使用して、ターゲット セクションがスクロール コンテナに表示されているときにスタイルを適用します。
  4. セマンティック状態: スクリプトを使用して、スクロールが停止したときにアクティブなリンクの aria-current="true" を更新し、状態が支援技術に正しく伝達されるようにします。

コード例

この例では、ユーザーがセクションをスクロールすると、固定ナビゲーション メニューのアクティブなリンクがハイライト表示されます。


/* MANDATORY: The container of markers must have scroll-target-group: auto */
nav {
  /* Enables automatic tracking of fragment links */
  scroll-target-group: auto;
}
/* MANDATORY: Use :target-current to style the link whose target is in view. */
nav a:target-current {
  color: var(--accent, #007bff);
  border-bottom-color: var(--accent, #007bff);
}
<nav>
  <a href="#intro">Introduction</a>
  <a href="#features">Features</a>
  <a href="#pricing">Pricing</a>
</nav>

<main>
  <!-- MANDATORY: Each section must have a unique ID matching a link fragment in the <nav>. -->
  <section id="intro">
    <h2>Introduction</h2>
    <!-- content -->
  </section>
  <section id="features">
    <h2>Features</h2>
    <!-- content -->
  </section>
  <section id="pricing">
    <h2>Pricing</h2>
    <!-- content -->
  </section>
</main>

<script type="module">
  if (CSS.supports('scroll-target-group: auto')) {
    const syncAriaCurrent = () => {
      const currentLink = document.querySelector('nav a:target-current');
      document.querySelectorAll('nav a').forEach(link => {
        link.setAttribute('aria-current', link === currentLink ? 'true' : 'false');
      });
    };
    // Update the aria on initial page load. Ensure the DOM content is loaded, either by using `<script type="module">` or an event listener for `DOMContentLoaded`.
    syncAriaCurrent();
    // Update the aria after scroll ends
    document.addEventListener('scrollend', syncAriaCurrent);
  }
</script>

ベスト プラクティス

  • 推奨: ナビゲーション リンク専用のコンテナで scroll-target-group: auto を使用します。これにより、ブラウザはどのリンクをトラッキングすべきかを認識できます。
  • 推奨: ターゲットに、リンクの href 属性と一致する一意の id 属性があることを確認します。
  • 推奨: セクションに十分な垂直方向のスペースを確保します。セクションが十分に大きく、一度に 1 つまたは 2 つしか表示されない場合、scroll-target-group は表示されているターゲットを最も明確に識別します。
  • 必須: ユーザー補助のため、視覚的な状態を aria-current でミラーリングする必要があります。:target-current は視覚的な側面を処理しますが、スクリーン リーダーには公開されません。
  • AVOID: アクティブな状態を色だけで示す。フォントの太さ、下線、インジケーター ドットなどの他の視覚的な手がかりを含めるか、色のコントラストを十分に確保して、色覚異常のあるユーザーでも違いを認識できるようにします。

代替戦略

scroll-target-group をサポートしていないブラウザでは、IntersectionObserver API に基づくフォールバックを使用する必要があります。

<script>
  // Feature detection for scroll-target-group
  const supportsScrollTargetGroup = CSS.supports('scroll-target-group: auto');

  if (!supportsScrollTargetGroup) {
    const navLinks = document.querySelectorAll('nav a');
    const sections = document.querySelectorAll('main section');

    const observer = new IntersectionObserver((entries) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const id = entry.target.getAttribute('id');
          navLinks.forEach(link => {
            const isActive = link.getAttribute('href') === `#${id}`;
            // Use the escaped colon class for consistent styling
            link.classList.toggle('\:target-current', isActive);
            link.setAttribute('aria-current', isActive);
          });
        }
      });
    }, { rootMargin: '-50% 0px -50% 0px', threshold: 0 });

    sections.forEach(section => observer.observe(section));
  }
</script>

<style>
  /* Fallback style if :target-current is not supported.
     Use :is() to combine the native pseudo-class with a fallback class for older browsers. DO NOT use `nav a:target-current, nav a.\:target-current, as the unrecognized pseudoselector invalidates the entire rule in older browsers. */
  nav a:is(:target-current, .\:target-current) {
    color: var(--accent, #007bff);
    border-bottom-color: var(--accent, #007bff);
    font-weight: bold;
  }
</style>