捲動貼齊狀態同步

以往,如要同步處理 UI 狀態與可捲動容器的貼齊位置,必須使用複雜的捲動事件監聽器、手動計算捲動偏移,以及交集觀察器。scrollsnapchange 事件提供原生且有效率的方法,可偵測捲動器何時停在新的貼齊目標上,因此可用於同步處理側欄,或醒目顯示目錄中的有效區段。

導入作業

1. 在 CSS 中設定捲動貼齊

容器必須定義 scroll-snap-type,且子項必須有 scroll-snap-align,瀏覽器才能追蹤貼齊目標。在含有目錄的長篇文章中,您可以使用這個屬性,將章節標題貼齊可視區域頂端。

main {
    /* Enable scroll snapping on the container */  
  scroll-snap-type: y proximity;
  overflow-y: auto;
}

h2 {
  /* Define how headers align when snapped */
  scroll-snap-align: start;
}

2. 監聽貼齊變更

在捲動容器上使用 scrollsnapchange 事件,在使用者完成捲動且瀏覽器貼齊新元素時做出反應。在目錄示範中,我們使用這個方法醒目顯示側欄中的有效連結。

<!-- MANDATORY: Wrap table of contents links inside a proper navigation landmark -->
<nav aria-label="Table of contents">
  <ul>
    <li><a href="#section-1" aria-current="location">Section 1</a></li>
    <li><a href="#section-2">Section 2</a></li>
  </ul>
</nav>
const main = document.getElementById('main');
const links = document.querySelectorAll('nav a');

// The event fires when the scroller settles on a new snap target
main.addEventListener('scrollsnapchange', (event) => {
  // Use snapTargetBlock for vertical or snapTargetInline for horizontal
  const snappedHeader = event.snapTargetBlock;
  
  if (snappedHeader) {
    setSelectedParagraph(snappedHeader.id);
  }
});

無障礙設定

如果是目錄,請確保側欄連結在啟用時使用 aria-current="true" 或 aria-current="location"。

此外,請謹慎使用 scroll-snap-type 的 mandatory 值,因為如果內容長度超過螢幕,可能會導致貼齊點之間的內容無法存取。

備用策略

如果系統不支援 scrollsnapchange,請使用 IntersectionObserver 偵測目前位於捲動器頂端的元素。請注意,這與 scrollsnapchange 的行為不同,因為這會在捲動時觸發,而不是在捲動完成時觸發。

// Feature detect support for scroll snap events
if (!('onscrollsnapchange' in HTMLElement.prototype)) {
  const observer = new IntersectionObserver(
    () => {
      // Each time the set of intersecting headers changes, find the top
      // header that is visible.
      const topEntry = [...headers].reduce((currentTop, header) => {
        // Use the bottom to handle scrolling up, when the top is still offscreen
        const {bottom} = header.getBoundingClientRect();
        // Don't match if the header's bottom is above the scrollport
        if (bottom < 0) return;
        if (!currentTop) return header;
        return bottom <
          currentTop.getBoundingClientRect().bottom
          ? header
          : currentTop;
      }, undefined);
      if (topEntry) setSelectedParagraph(topEntry.id);
    },
    { root: main, threshold: 0.9 // Adjust based on your use case },
  );

  // Observe all snap targets (e.g., section headers)
  document.querySelectorAll('h2').forEach(header => observer.observe(header));
}