滚动贴靠状态同步

传统上,若要将界面状态与可滚动容器的贴靠位置同步,需要复杂的滚动事件监听器、手动计算滚动偏移量以及交叉观察器。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 事件,以便在用户完成滚动且浏览器贴靠到新元素时做出响应。在我们的 TOC 演示中,我们使用此功能突出显示边栏中的活动链接。

<!-- 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));
}