
本文介绍如何利用原生滚动事件监听与 dom 位置计算,在页面滚动过程中动态高亮与视口顶部最接近的子导航菜单项,实现精准、平滑的滚动锚点激活效果。
本文介绍如何利用原生滚动事件监听与 dom 位置计算,在页面滚动过程中动态高亮与视口顶部最接近的子导航菜单项,实现精准、平滑的滚动锚点激活效果。
在构建单页长内容网站(如产品介绍页、文档页或营销落地页)时,一个固定在顶部的子导航栏(subnav)常用于快速跳转至不同章节。但仅支持点击跳转还不够——更专业的体验是:当用户滚动到某区块(如 #div1)附近时,对应导航项自动高亮,形成视觉反馈与上下文感知。
上述需求看似简单,但使用 IntersectionObserver 直接监听整个 .content 容器往往不够精确(因其触发范围过大),而基于 scroll 事件 + 偏移量比对的方式则更可控、兼容性好、逻辑清晰。
✅ 核心实现思路
- 监听全局滚动事件:在 mounted 中绑定 window.scroll;
- 动态计算各目标区块的“激活阈值”:以固定导航栏高度为偏移基准,计算每个 #divX 距离视口顶部的“有效起始位置”(即 offsetTop - stickyNavHeight);
- 实时比对滚动位置:用 window.scrollY 与各阈值比较,判断当前最靠近导航栏的是哪个区块;
- 响应式更新状态与样式:通过 Vue 的响应式数据 activeSection 驱动 <a> 标签的 :class="{ active: ... }" 动态类名;
- 增强交互体验:点击导航项时,自动滚动至对应区块顶部,并预留导航栏高度空间(避免被遮挡)。
? 示例代码(Vue 2 兼容版)
<template>
<div>
<div class="sticky-nav">
<a :class="{ active: activeSection === 'div1' }" @click="scrollToSection('div1')">Menu 1</a>
<a :class="{ active: activeSection === 'div2' }" @click="scrollToSection('div2')">Menu 2</a>
<a :class="{ active: activeSection === 'div3' }" @click="scrollToSection('div3')">Menu 3</a>
</div>
<div class="content">
<div class="content-div" id="div1">Section 1 — Introduction</div>
<div class="content-div" id="div2">Section 2 — Features</div>
<div class="content-div" id="div3">Section 3 — Pricing</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
activeSection: null
};
},
mounted() {
window.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
window.removeEventListener('scroll', this.handleScroll);
},
methods: {
handleScroll() {
const nav = document.querySelector('.sticky-nav');
if (!nav) return;
const stickyNavHeight = nav.clientHeight;
const sections = ['div1', 'div2', 'div3'];
const scrollY = window.scrollY;
// 从后往前遍历,确保最后一个匹配项优先(避免被前序覆盖)
let matched = null;
for (let i = sections.length - 1; i >= 0; i--) {
const el = document.getElementById(sections[i]);
if (!el) continue;
const top = el.offsetTop - stickyNavHeight;
if (scrollY >= top) {
matched = sections[i];
break;
}
}
this.activeSection = matched;
},
scrollToSection(sectionId) {
const el = document.getElementById(sectionId);
if (!el) return;
const navHeight = document.querySelector('.sticky-nav').clientHeight;
window.scrollTo({
top: el.offsetTop - navHeight,
behavior: 'smooth'
});
}
}
};
</script>
<style scoped>
.sticky-nav {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #fff;
padding: 12px 20px;
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
z-index: 1000;
display: flex;
align-items: center;
}
.sticky-nav a {
margin: 0 16px;
text-decoration: none;
color: #555;
font-size: 14px;
transition: color 0.2s;
}
.sticky-nav a.active {
color: #2563eb;
font-weight: 600;
border-bottom: 2px solid #2563eb;
}
.content {
padding-top: 64px; /* 确保首屏内容不被 sticky-nav 遮挡 */
}
.content-div {
min-height: 70vh;
padding: 40px 20px;
font-size: 18px;
line-height: 1.6;
}
</style>⚠️ 注意事项与优化建议
- 性能考量:scroll 事件高频触发,建议使用 throttle(节流)进一步优化(如 Lodash 的 _.throttle(handleScroll, 100)),尤其在复杂布局中;
- 动态内容适配:若页面内容由异步加载或 v-if 控制,需在 DOM 更新后重新计算 offsetTop,可配合 $nextTick() 或 ResizeObserver 监听尺寸变化;
- 无障碍支持:为 .sticky-nav a.active 添加 aria-current="section" 属性,提升屏幕阅读器体验;
- 多级嵌套/动态 ID:可将 sections 数组改为通过 document.querySelectorAll('[data-nav-id]') 动态获取,提高可维护性;
- Vue 3 用户注意:将 beforeDestroy 替换为 onBeforeUnmount,并使用 onMounted / onBeforeUnmount 组合式 API 重构。
通过以上方案,你不仅能精准实现滚动高亮,还能获得良好的可扩展性与用户体验一致性——无需第三方库,纯原生能力即可交付专业级导航交互。

















