You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

响应式布局中监听滚动位置,实现侧边导航aria-current切换

解决方案

一、无JS方案的可行性

不存在纯HTML/CSS的无JS方案能实现动态添加/移除aria-current="true"属性。原因是aria-current属于DOM属性,纯前端静态技术无法修改DOM属性——虽然可以用:target伪类给锚点对应的导航项添加视觉选中样式,但这只是视觉层面的模拟,无法满足语义化的ARIA属性要求,因此必须使用JavaScript实现。

二、基于IntersectionObserver的实现方案

按照你的需求,我们可以通过不可见触发元素+自定义相交区域的方式,精准监听章节是否到达导航栏下方50-75px的范围,同时处理锚点跳转后的状态同步。

1. 核心思路

  • 给每个带ID的章节上方插入不可见的触发div,作为相交检测的标记点;
  • 配置IntersectionObserver的rootMargin,将相交区域限定在导航栏下方60px(取50-75的中间值)的水平线位置;
  • 监听触发元素的相交状态,动态给对应导航项添加/移除aria-current;
  • 监听锚点跳转事件,主动同步导航项状态,确保跳转后即使章节不在触发区域,也能保持选中直到滚动离开。

2. 代码实现

HTML结构

<!-- 固定导航栏 -->
<header class="navbar" style="height: 60px; position: fixed; top: 0; width: 100%; background: #fff;">导航栏</header>

<!-- 侧边栏导航 -->
<aside class="sidebar" style="position: fixed; left: 0; top: 60px; width: 200px;">
  <nav>
    <ul style="list-style: none; padding: 0;">
      <li><a href="#section1" class="nav-link">章节1</a></li>
      <li><a href="#section2" class="nav-link">章节2</a></li>
      <li><a href="#section3" class="nav-link">章节3</a></li>
    </ul>
  </nav>
</aside>

<!-- 内容区域 -->
<main class="content" style="margin-left: 220px; margin-top: 60px;">
  <!-- 不可见触发元素 -->
  <div class="section-trigger" data-section-id="section1"></div>
  <section id="section1" style="height: 800px; padding: 20px;">
    <h2>章节1内容</h2>
  </section>

  <div class="section-trigger" data-section-id="section2"></div>
  <section id="section2" style="height: 800px; padding: 20px;">
    <h2>章节2内容</h2>
  </section>

  <div class="section-trigger" data-section-id="section3"></div>
  <section id="section3" style="height: 800px; padding: 20px;">
    <h2>章节3内容</h2>
  </section>
</main>

<!-- 页脚 -->
<footer class="footer" style="height: 200px; background: #eee;">页脚</footer>

CSS样式

/* 隐藏触发元素,仅用于相交检测 */
.section-trigger {
  position: absolute;
  top: -60px; /* 与导航栏高度对齐,确保触发点在章节上方、导航栏下方 */
  width: 100%;
  height: 1px; /* 最小高度保证能被Observer检测到 */
  visibility: hidden;
}

.content {
  position: relative; /* 确保触发元素的绝对定位基于内容区域 */
}

JavaScript逻辑

// 获取导航栏高度,适配响应式变化
function getNavbarHeight() {
  return document.querySelector('.navbar').offsetHeight;
}

// 初始化/更新IntersectionObserver
function initSectionObserver() {
  // 清除旧的观察者(避免重复监听)
  if (window.sectionObserver) {
    window.sectionObserver.disconnect();
  }

  const navbarHeight = getNavbarHeight();
  // 自定义相交区域:导航栏下方60px的水平线
  // rootMargin计算:顶部偏移-(导航栏高度+60px),底部偏移-(视口高度-导航栏高度-60px)
  const rootMargin = `-${navbarHeight + 60}px 0 calc(-100vh + ${navbarHeight} + 60px) 0`;

  const observer = new IntersectionObserver((entries) => {
    entries.forEach(entry => {
      const sectionId = entry.target.dataset.sectionId;
      const navLink = document.querySelector(`.nav-link[href="#${sectionId}"]`);
      if (!navLink) return;

      if (entry.isIntersecting) {
        // 移除所有导航项的aria-current
        document.querySelectorAll('.nav-link[aria-current="true"]').forEach(link => {
          link.removeAttribute('aria-current');
        });
        // 给当前章节对应的导航项添加属性
        navLink.setAttribute('aria-current', 'true');
      } else {
        // 滚动离开章节区域时移除属性
        navLink.removeAttribute('aria-current');
      }
    });
  }, {
    root: null, // 以视口为根元素
    rootMargin: rootMargin,
    threshold: 0 // 只要触发元素进入相交区域就触发
  });

  // 监听所有触发元素
  document.querySelectorAll('.section-trigger').forEach(trigger => {
    observer.observe(trigger);
  });

  window.sectionObserver = observer;
}

// 处理锚点跳转后的导航状态同步
function syncNavWithHash() {
  const hash = window.location.hash.slice(1);
  if (!hash) return;

  document.querySelectorAll('.nav-link[aria-current="true"]').forEach(link => {
    link.removeAttribute('aria-current');
  });

  const targetLink = document.querySelector(`.nav-link[href="#${hash}"]`);
  if (targetLink) {
    targetLink.setAttribute('aria-current', 'true');
  }
}

// 绑定事件监听
document.addEventListener('DOMContentLoaded', () => {
  initSectionObserver();
  syncNavWithHash();

  // 监听窗口大小变化,更新观察者(适配导航栏高度变化)
  window.addEventListener('resize', initSectionObserver);

  // 监听锚点变化
  window.addEventListener('hashchange', syncNavWithHash);

  // 监听导航链接点击,确保跳转后状态同步
  document.querySelectorAll('.nav-link').forEach(link => {
    link.addEventListener('click', () => {
      setTimeout(syncNavWithHash, 100); // 等待页面跳转完成后同步
    });
  });
});

3. 关键细节说明

  • 响应式适配:通过resize事件重新计算导航栏高度,更新Observer的rootMargin,确保在不同视口下相交区域始终精准;
  • 触发元素设计:使用不可见的小高度div作为触发点,避免章节标题高度变化影响检测精度;
  • 锚点跳转处理:同时监听hashchange和导航链接点击事件,主动同步导航状态,确保跳转后即使章节不在触发区域,也能保持选中直到用户滚动离开。

内容的提问来源于stack exchange,提问作者jimmymcheung

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.24 14:48:16