构建 Scrollspy 导航

滚动监听导航是一种常见的界面模式,可自动突出显示与当前视口中的页面部分对应的导航链接。这样,用户在滚动浏览长文档时,就能清楚地知道自己所在的位置。

此模式在以下两种场景中很常见:

  • 单页网站:主网站导航链接到单个网页的不同部分,而不是单独的网址。
  • 长篇内容:通过目录或导航边栏跟踪用户在文章、指南或文档中的阅读进度。

如何实现

传统上,滚动监听模式需要 JavaScript 和 IntersectionObserver API。现代 CSS 提供了一种原生方式,可以使用 scroll-target-group 属性和 :target-current 伪类来实现此目的。

  1. 确保 Navigation Links 使用 Fragment Identifiers:您的 Navigation 必须由锚链接 (<a>) 组成,这些链接使用与内容部分 id 匹配的 Fragment Identifiers(例如 <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>