ScrollSpy
基础导航基于 Bootstrap 5 的滚动监听组件,使用 IntersectionObserver 跟踪区域几何信息并结合滚动位置实时计算激活状态,支持 rootMargin/threshold 观察配置、平滑滚动、受控模式、上下文式链接(ScrollSpyLink)与普通 HTML 导航(target 选择器)两种接入方式
Component Demo
Basic usage and examples of the ScrollSpy component
基础用法
第一个标题
这是第一个区域的内容。向下滚动容器,每个区域的标题会依次越过容器顶部的激活线,导航中的对应链接随之自动高亮。 当前标题越过激活线后,其链接会一直保持激活,直到下一个标题到达,确保激活项始终跟随阅读位置。
第二个标题
这是第二个区域的内容。激活状态由滚动位置实时计算,切换时机由 rootMargin 顶部边距决定的激活线控制。继续向下滚动,后续区域的标题会依次越过激活线,导航激活项随之逐个切换。
第三个标题
这是第三个区域的内容。向上滚动时,激活项会按照相反的顺序逐个切换回前面的区域,与阅读位置保持一致。 点击上方导航链接可以直接跳转到对应区域,跳转后激活项立即同步。
第四个标题
这是第四个区域的内容。每个区域都包含足够的内容,保证在滚动过程中后一个区域的标题能够到达激活线, 从而依次触发导航切换,中间不会出现空档或跳项。
第五个标题
这是最后一个区域的内容。当容器滚动到底部时,最后一个链接会自动激活(与 Bootstrap 的 maxScroll 行为一致),保证导航始终有选中的项。
ScrollSpy 提供上下文,ScrollSpyLink 根据当前激活 id 自动渲染 active 类;ScrollSpyContainer 默认渲染 `position: relative` 的可滚动容器(高度与 overflow 由使用方样式控制);底层通过 IntersectionObserver 跟踪各区域的几何信息,结合滚动位置实时计算激活项
嵌套导航(普通 HTML 目标)
第一个
这是第一个区域的内容。向下滚动容器,本区域的两个子区域标题会依次越过激活线, 左侧嵌套导航中对应的链接随之激活。
第一个-1
这是“第一个-1”子区域的内容。子区域与普通区域共用同一套激活机制,切换顺序由标题越过激活线的先后决定。
第一个-2
这是“第一个-2”子区域的内容。继续向下滚动,激活项会切换到下一个子区域或区域。
第二个
这是第二个区域的内容。向上滚动时,激活项会依次切换回前面的区域与子区域,与阅读位置保持一致。
第三个
这是第三个区域的内容,同样包含两个子区域。滚动到子区域标题越过激活线时,对应链接会自动激活。
第三个-1
这是“第三个-1”子区域的内容。本例的导航是普通 HTML,组件通过 target 选择器直接管理其中链接的 active 类。
第三个-2
这是“第三个-2”子区域的内容。滚动到容器底部时,最后一个链接会自动激活,保证导航始终有选中的项。
传入 target 选择器(等价于 Bootstrap 的 data-bs-target)后,组件自动收集目标元素中所有带锚点 href 的链接,并按 Bootstrap 的行为直接管理其 active 类,导航可以是任意普通 HTML
列表组目标
列表项 1
这是第一个列表项对应的内容区域。向下滚动容器,后续列表项的标题会依次越过激活线, 左侧列表组中对应的项随之激活。
列表项 2
这是第二个列表项对应的内容区域。激活的列表项会渲染 active 类,与 Bootstrap 中 list-group-item 的激活样式保持一致。
列表项 3
这是第三个列表项对应的内容区域。继续向下滚动即可切换到下一个列表项,向上滚动则依次返回。
列表项 4
这是第四个列表项对应的内容区域。滚动到底部时,最后一个列表项自动激活,保证导航始终有选中的项。
与 Bootstrap 一致,target 中的任意 `[href]` 元素(如 list-group-item)都能作为激活目标, 激活时为其添加 active 类
观察配置(rootMargin 与 threshold)
区域一
这是区域一的内容。激活线与可见比例变化后,激活切换的时机也会随之改变,向下滚动观察切换位置的变化。
区域二
这是区域二的内容。点击左侧导航链接可以平滑滚动到对应区域,滚动停止后激活项立即与目标区域同步。
区域三
这是区域三的内容。rootMargin 顶部边距决定激活线的初始位置,激活线随滚动逐渐下移, 底部边距与 threshold 决定可见比例门槛。
区域四
这是区域四的内容。修改上方下拉框可以实时调整观察配置,滚动到底部时最后一项自动激活。
顶部边距决定激活线的位置;当滚动行程不足以让所有章节依次越过激活线时,激活线会自动下移, 保证每个区域都能依次激活;底部边距与 threshold 决定区域在可见区域内的比例门槛;开启 smoothScroll 后点击导航链接会平滑滚动,且自动遵循系统的减少动态效果设置
受控模式
区域一
这是第一个区域的内容。当前激活的区域由外部 state 控制,滚动容器时 onActivate 回调会更新该 state,激活项跟随阅读位置变化。
区域二
这是第二个区域的内容。点击上方按钮可以跳转到对应区域,按钮复用了 ScrollSpyLink 的滚动逻辑,跳转后激活项立即同步。
区域三
这是第三个区域的内容。滚动到容器底部时,最后一个链接自动激活,与 Bootstrap 的 maxScroll 行为一致。
当前激活的区域:spy-controlled-1
传入 activeId 后 ScrollSpy 变为受控组件,激活状态完全由 onActivate 驱动的外部 state 决定; 上方按钮通过 ScrollSpyLink 的按钮样式复用点击滚动逻辑
页面滚动监听
区域一
当 ScrollSpyContainer 未设置滚动溢出样式(overflow-y 为 visible)时,监听根自动切换为浏览器视口,此时滚动整个页面即可切换导航激活状态,等价于 Bootstrap 中在 body 上初始化 ScrollSpy。上方导航在页面滚动时固定在视口顶部,方便观察激活变化。
区域二
继续向下滚动页面,“区域二”的标题滚动到视口顶部(激活线)时,导航中的对应链接会被激活。 每个区域的标题越过激活线后,其链接保持激活,直到下一个标题到达。
区域三
激活线的位置由 rootMargin 的顶部边距决定,可见比例门槛由 threshold 决定。向上滚动页面时,激活项会依次切换回前面的区域。
区域四
这是最后一个区域的内容。页面滚动到底部时,最后一个链接自动激活(与 Bootstrap 在 body 上使用 ScrollSpy 的 maxScroll 行为一致)。
与 Bootstrap 的 ScrollSpy 实现一致:当监听容器 overflow-y 为 visible 时,观察根回退为浏览器视口, 向下滚动本页面即可看到导航激活状态的变化
不可见元素与禁用链接
可见区域
这是一个正常可见的区域,向下滚动时它会正常激活对应链接。本区域包含足够的内容, 保证后续区域能够依次到达激活线。
隐藏区域
设置了 display: none 的区域不会被监听,因此“隐藏区域”链接永远不会被激活。
另一个区域
滚动到这里时,“另一个区域”链接会被激活。“禁用链接”由于带 disabled 类被跳过,始终保持不可交互状态。
与 Bootstrap 一致:不可见(display: none 等)的区域不会注册观察;带 disabled 属性或 disabled 类的链接会被跳过,不会参与激活
API Documentation
Complete API reference for the ScrollSpy component
Props
ScrollSpy
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
activeId | string | - | 受控激活 id,与 ScrollSpyLink 的 targetId(或 href 锚点)匹配时对应链接自动渲染 active 类,需配合 onActivate 更新 |
defaultActiveId | string | - | 非受控模式下的初始激活 id,设置后滚动监听会自动维护激活状态 |
onActivate | (activeId: string | null, link: HTMLElement | null) => void | - | 激活回调,新区域的标题滚动到激活线时以 (区域 id, 对应链接元素) 触发;滚动到所有区域上方(无区域越过激活线)时以 (null, null) 触发(Bootstrap 仅在激活时触发 activate 事件,此处为 React 受控适配) |
rootMargin | string | '0px 0px -25%' | 观察配置(与 Bootstrap 的 rootMargin 选项一致):顶部边距决定激活线的位置(默认 0,即滚动容器顶部/视口顶部);当部分章节的标题无法在滚动行程内越过激活线时,激活线会自动逐渐下移,保证每个章节都能依次激活(章节足够高时保持经典行为不变);底部边距收缩可见区域范围,配合 threshold 控制可见比例门槛 |
smoothScroll | boolean | false | 点击 ScrollSpyLink 时平滑滚动到目标区域(等价于 Bootstrap 的 smoothScroll 选项),并自动遵循 prefers-reduced-motion |
target | string | - | 选择器字符串,等价于 Bootstrap 的 data-bs-target:指定普通 HTML 导航元素,自动收集其中带锚点 href 的链接并直接管理其 active 类,适合静态导航、嵌套导航与列表组;点击其中链接时保留原生锚点跳转(URL hash 同步),并在跳转后让点击的链接保持激活(即使目标标题无法滚动到激活线) |
threshold | number | number[] | [0.1, 0.5, 1] | 可见比例门槛:区域在(经 rootMargin 收缩后的)可见区域内的可见比例达到该值后才可激活;传入数组时取最小值作为门槛 |
children | ReactNode | - | 由 ScrollSpyContainer 与导航(含 ScrollSpyLink)组成的内容 |
ScrollSpyContainer
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
as | ElementType | 'div' | 渲染的元素标签,等价于 Bootstrap 中带 data-bs-spy="scroll" 的滚动容器;默认只设置 position: relative,滚动溢出样式(overflow/height)由使用方控制 |
tabIndex | number | 0 | 键盘焦点序号,让容器可以通过键盘滚动(Bootstrap 文档建议设置 tabindex="0") |
ScrollSpyLink
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
targetId | string | - | 目标区域 id,未设置时从 href 的 # 锚点解析;在 ScrollSpy 内渲染时自动注册监听并激活 |
href | string | #targetId | 链接地址,未设置时自动生成 `#targetId`;以 # 开头时参与滚动监听,其他 href 渲染为普通链接 |
smoothScroll | boolean | - | 覆盖 ScrollSpy 的 smoothScroll 设置,单独控制该链接点击时的滚动行为 |
onClick | (event: MouseEvent<HTMLElement>) => void | - | 点击回调,触发后再执行默认滚动逻辑;调用 event.preventDefault() 可阻止滚动 |
as | ElementType | 'a' | 渲染的元素标签,便于接入路由组件等自定义链接 |
Common Props
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | 组件内容 |
className | string | - | 自定义类名 |
...rest | HTMLAttributes | - | 透传原生元素属性(如 `style`、`role` 等) |
Type Definitions
ScrollSpyProps
滚动监听组件属性接口
export interface ScrollSpyProps {
activeId?: string;
children?: ReactNode;
defaultActiveId?: string;
onActivate?: (activeId: string | null, link: HTMLElement | null) => void;
rootMargin?: string;
smoothScroll?: boolean;
target?: string;
threshold?: number | number[];
}ScrollSpyContainerProps
滚动监听容器组件属性接口
export interface ScrollSpyContainerProps extends HTMLAttributes<HTMLElement> {
as?: ElementType;
children?: ReactNode;
className?: string;
tabIndex?: number;
}ScrollSpyLinkProps
滚动监听链接组件属性接口
export interface ScrollSpyLinkProps extends Omit<HTMLAttributes<HTMLElement>, 'onClick'> {
as?: ElementType;
children?: ReactNode;
className?: string;
href?: string;
onClick?: (event: MouseEvent<HTMLElement>) => void;
smoothScroll?: boolean;
targetId?: string;
}ScrollSpyContextValue
滚动监听上下文,供 ScrollSpyContainer 与 ScrollSpyLink 消费
export interface ScrollSpyContextValue {
activeId: string | null;
registerContainer: (element: HTMLElement | null) => void;
registerLink: (targetId: string) => () => void;
requestScroll: (targetId: string, smooth: boolean) => void;
smoothScroll: boolean;
}