锚点
概述
Anchor 用于长内容区域的页内导航,并会在滚动时自动高亮当前所在的章节。
滚动激活项变化时,地址栏中的 hash 也会同步更新;滚动同步使用 history.replaceState,避免产生过多历史记录。
如果页面初始化时地址栏已经带有 hash,Anchor 会在挂载后自动滚动到对应章节;当自定义滚动容器稍后才就绪时,也会在容器可用后重新同步一次。
SAnchor现在会把递归的锚点项渲染委托给 AriaAnchorCompact。SAnchor与@vean/aria/anchor共享同一组 6 个ui槽位。
功能特性
- 滚动侦测高亮 — 根组件监听滚动容器(默认
window,可用getContainer指定自定义元素),将最近一个越过offsetTop + bounds阈值的章节标记为激活,并在对应链接上输出aria-current="location"。 - Hash 同步 — 点击链接通过
history.pushState写入 hash(设置replace时改用replaceState);滚动驱动的高亮更新始终使用replaceState;挂载时若 URL 已带 hash 会先滚动到对应章节。 - 平滑页内滚动 — 点击 hash 链接以
behavior: 'smooth'滚动容器,并在动画收敛前(300ms 守卫)暂停滚动侦测,避免中间态抖动。 - 自定义滚动容器 —
getContainer返回任意可滚动的HTMLElement;锚点位置相对该容器测量,且容器替换初始window兜底后会自动重新同步。 - 偏移控制 —
offsetTop(默认0)与targetOffset(优先级更高)调整滚动目标,使标题落在吸顶头部下方;该值同时以--vean-anchor-offset-top暴露给吸顶根节点。 - 激活判定调优 —
bounds(默认5)为判定章节是否越过的像素容差;getCurrentAnchor可在值对外发出前重新映射。 - 递归数据驱动组合 —
SAnchor通过泛型 AriaAnchorCompact渲染任意层级的嵌套items,每层包裹自己的AnchorLink、指示器、标题与嵌套sub列表。 - 吸顶导轨 —
sticky(默认true)将锚点列表钉在容器顶部(top: var(--vean-anchor-offset-top))并把高度限制在视口内。 - 逐项与全局链接属性 —
disabled与target既可按项设置,也可通过linkProps全局兜底;显式的项值始终优先,linkProps.href被刻意排除在类型之外(目标地址由项的href决定)。 - 八种主题色 + 六种尺寸 —
color(primary…accent)与size(xs…2xl)变体来自anchorVariantsscv()配方,另支持纵向 / 横向orientation。 - 默认无障碍 — 根节点渲染带本地化
aria-label(可覆盖)的<nav>地标;禁用链接携带aria-disabled并移出 Tab 顺序;指示器aria-hidden。 - Aria 组合 —
AnchorRoot/AnchorLink/AnchorCompact/AnchorItemCompact均可从@vean/aria/anchor导出,用于完全自定义样式。
用法
SAnchor现在将递归项渲染委托给 AriaAnchorCompact。无样式的数据驱动用法可导入@vean/aria/anchor的AnchorCompact。
演示
基础
Overview
Scroll the content area to watch the active anchor update automatically.
Usage
Scroll the content area to watch the active anchor update automatically.
Installation
Scroll the content area to watch the active anchor update automatically.
Configuration
Scroll the content area to watch the active anchor update automatically.
API
Scroll the content area to watch the active anchor update automatically.
自定义样式
API
Aria 层
5Anchor
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
color | ThemeColor | — | 组件的主题颜色。 |
size | ThemeSize | — | 组件的视觉尺寸。 |
ui | Partial<AnchorExtendedUi> | — | 该组件的按槽类重写。 |
class | ClassValue | — | 应用于根元素的额外类名。 |
sticky | boolean | — | 是否为置顶内容。 |
items | AnchorOptionData[] | — | 由该组件渲染的项。 |
linkProps | Omit<AnchorLinkProps, 'href'> | — | 传递给 link 元素的属性。 |
indicatorProps | HTMLAttributes | — | 转发给 indicator 元素的属性。 |
titleProps | HTMLAttributes | — | 传递给 title 元素的属性。 |
subProps | HTMLAttributes | — | 转发给子元素的属性。 |
bounds | number | — | 限制条件。 |
dir | Direction | — | 组件的阅读方向。 |
getContainer | (() => AnchorContainer | null) | — | 获取容器。 |
getCurrentAnchor | ((activeHref: string) => string) | — | 获取当前锚点。 |
modelValue | string | — | 当前模型值。 |
offsetTop | number | — | 顶部偏移。 |
orientation | DataOrientation | — | 组件的定位。 |
replace | boolean | — | 是否替换。 |
targetOffset | number | — | 目标偏移量。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
update:modelValue | string | 当模型值发生变化时触发。 |
activeChange | string | 当发生有效更改时触发。 |
itemSelect | MouseEvent, payload: { href: string; } | 在发生项选择时触发。 |
AnchorCompact
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
items | AnchorOptionData[] | — | 由该组件渲染的项。 |
linkProps | Omit<AnchorLinkProps, 'href'> | — | 传递给 link 元素的属性。 |
indicatorProps | HTMLAttributes | — | 转发给 indicator 元素的属性。 |
titleProps | HTMLAttributes | — | 传递给 title 元素的属性。 |
subProps | HTMLAttributes | — | 转发给子元素的属性。 |
bounds | number | — | 限制条件。 |
dir | Direction | — | 组件的阅读方向。 |
getContainer | (() => AnchorContainer | null) | — | 获取容器。 |
getCurrentAnchor | ((activeHref: string) => string) | — | 获取当前锚点。 |
modelValue | string | — | 当前模型值。 |
offsetTop | number | — | 顶部偏移。 |
orientation | DataOrientation | — | 组件的定位。 |
replace | boolean | — | 是否替换。 |
targetOffset | number | — | 目标偏移量。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
update:modelValue | string | 当模型值发生变化时触发。 |
activeChange | string | 当发生有效更改时触发。 |
itemSelect | MouseEvent, payload: { href: string; } | 在发生项选择时触发。 |
AnchorItemCompact
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
modelValue | string | — | 当前模型值。 |
item | AnchorOptionData | — | 当前条目数据。 |
linkProps | Omit<AnchorLinkProps, 'href'> | — | 传递给 link 元素的属性。 |
indicatorProps | HTMLAttributes | — | 转发给 indicator 元素的属性。 |
titleProps | HTMLAttributes | — | 传递给 title 元素的属性。 |
subProps | HTMLAttributes | — | 转发给子元素的属性。 |
AnchorLink
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
href | string | — | 锚点链接 |
disabled | boolean | — | 该组件是否已禁用。 |
to | string | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric | — | Route Location:点击该链接后应跳转到的位置。 |
external | boolean | — | 强制将链接视为外部链接(true)或内部链接(false)。这有助于处理边界情况 |
target | (string & {}) | '_blank' | '_parent' | '_self' | '_top' | null | — | 将链接的 URL 作为浏览上下文的名称显示在何处。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
AnchorRoot
属性
| 属性名 | 类型 | 默认值 | 描述 |
|---|---|---|---|
bounds | number | — | 限制条件。 |
dir | Direction | — | 组件的阅读方向。 |
getContainer | (() => AnchorContainer | null) | — | 获取容器。 |
getCurrentAnchor | ((activeHref: string) => string) | — | 获取当前锚点。 |
modelValue | string | — | 当前模型值。 |
offsetTop | number | — | 顶部偏移。 |
orientation | DataOrientation | — | 组件的定位。 |
replace | boolean | — | 是否替换。 |
targetOffset | number | — | 目标偏移量。 |
asChild | boolean | — | 将默认渲染元素替换为作为子元素传递的元素,并合并两者的属性及行为。 |
as | AsTag | Component | 'div' | 该组件应渲染为的元素或组件。可通过 `asChild` 进行覆盖。 |
事件
| 事件名 | 参数 | 描述 |
|---|---|---|
update:modelValue | string | 当模型值发生变化时触发。 |
activeChange | string | 当发生有效更改时触发。 |
itemSelect | MouseEvent, payload: { href: string; } | 在发生项选择时触发。 |
备注
架构与行业对标
| 能力 | VeanUI | Ant Design Anchor | Element Plus Anchor |
|---|---|---|---|
| Aria/styled 分离 | ✅ @vean/aria/anchor + scv() | ❌ 单包 | ❌ 单包 |
| 数据驱动 compact API | ✅ 泛型 AnchorCompact + 递归 items | ✅ items | ✅ items |
| 滚动容器 | ✅ getContainer(元素或 window) | ✅ getContainer | ✅ container |
| 滚动偏移 | ✅ offsetTop + targetOffset | ✅ offsetTop + targetOffset | ✅ offset |
| 激活容差 | ✅ bounds | ✅ bounds | ✅ bound |
| 激活值映射 | ✅ getCurrentAnchor | ✅ getCurrentAnchor | — |
| Hash 同步 | ✅ 点击 pushState / 滚动 replaceState | ✅ | ✅ |
| 初始 hash 滚动 | ✅ | ✅ | — |
| 吸顶导轨 | ✅ CSS sticky + 偏移变量 | ✅ affix 包裹层 | ✅ CSS sticky |
| 嵌套项 | ✅ 递归(任意层级) | ❌ 扁平 | ✅ 2 层 |
| 禁用项 | ✅ 逐项 + linkProps 兜底 | ❌ | ❌ |
| 自定义链接属性 | ✅ linkProps / indicatorProps / titleProps / subProps | — | — |
| 方向 | ✅ 纵向 / 横向 | ❌ 仅纵向 | ✅ direction |
| 主题色 / 尺寸 | ✅ 8 色 × 6 尺寸 | ❌ | ❌ |
| 事件 | ✅ activeChange / itemSelect | ✅ onChange / onClick | ✅ change |
运行时注意事项
- 测量时机 — 每次滚动事件都会基于当前滚动容器重新测量锚点位置;尚未渲染或未注册的链接会被跳过,激活项只从已注册链接中推导。
- 受控 / 非受控 — 提供
modelValue时,内部写入仅触发update:modelValue;高亮跟随 prop,外部改值不会触发滚动。 getContainer稳定性 — 容器引用变化时会重新同步;若每次调用都返回新元素会反复重同步,异步创建元素时请做好记忆化。- 仅 hash 链接 — 只有以
#开头的链接参与滚动与 hash 更新;普通/外链交由浏览器默认导航(itemSelect事件仍然触发)。 - 滚动动画守卫 — 程序化滚动后的 300ms 内暂停滚动侦测,避免中间位置误切激活项或改写 hash。
- 吸顶导轨 —
sticky需要存在可滚动的祖先才能真正钉住;偏移变量让导轨位于吸顶头部下方。不需要钉住时可设sticky: false。 - 禁用链接 — 禁用项完全惰性:
aria-disabled="true"、tabindex="-1",点击与键盘激活均被阻断(不触发itemSelect);仍保持注册以便子项高亮。
常见问题
如何滚动内部容器而不是 window?
传入 getContainer={() => myScrollableEl}。锚点位置将相对该元素测量,滚动也作用于它。当元素在挂载后才可用时,Anchor 会自动重新同步。
如何让标题落在吸顶头部下方?
把 offsetTop 设为头部高度。SAnchor 会滚动到 元素顶部 - offsetTop,并把同一值暴露为 --vean-anchor-offset-top,吸顶锚点导轨也会避开头部。若锚点偏移需要与滚动偏移不同,使用 targetOffset。
为什么滚动时地址栏会更新?
这是刻意设计——滚动驱动的高亮变化使用 history.replaceState,让 URL hash 始终与视口内章节一致且不刷历史;点击跳转默认使用 pushState(除非设置 replace)。
激活章节是如何选中的?
章节的测量顶部达到 offsetTop + bounds 即视为已越过;在所有已越过章节中取最接近阈值(最近越过)的一个高亮。bounds 增大容差;getCurrentAnchor 可在值发出前重映射。
可以禁用或自定义单个链接吗?
可以——每个项支持 disabled 与 target,全局默认通过 linkProps 设置。由于目标地址由项的 href 决定,linkProps.href 不在类型中。indicatorProps / titleProps / subProps 会把 HTML 属性转发到对应元素。
嵌套组合长什么样?
每个项渲染一个 AnchorLink(指示器 + 标题),有 children 时再渲染一个 sub 容器,内部递归渲染 AnchorItemCompact 层级。包装层带 data-vean-anchor-item,其 data-state 反映该项或其任一后代是否激活。