建構 Scrollspy 導覽

Scrollspy 導覽是一種常見的 UI 模式,會自動醒目顯示與目前檢視區中網頁部分對應的導覽連結。這樣一來,使用者在捲動長篇文件時,就能清楚知道自己所在的位置。

這種模式常見於以下兩種情況:

  • 單頁網站:主要網站導覽連結會連往單一網頁的不同部分,而非個別網址。
  • 長篇內容:目錄或側欄導覽會追蹤使用者在文章、指南或說明文件中的閱讀進度。

導入方式

傳統上,Scrollspy 模式需要 JavaScript 和 IntersectionObserver API。現代 CSS 提供原生方式,可使用 scroll-target-group 屬性和 :target-current 虛擬類別達成這項效果。

  1. 確保導覽連結使用片段 ID:導覽必須由錨點連結 (<a>) 組成,並使用與內容區段 id 相符的片段 ID (例如 <a href="#section-1">) 指向同一頁面上的區段。
  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 屬性。
  • 建議做法:為各部分提供足夠的垂直空間:當各部分夠大,一次只能顯示一或兩個部分時,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>