搜索 K
主题
@vaultkit/vitepress-plugin-scroll-navigator 是右下角「回顶部 / 到底部」浮钮组 Vue 组件。它解决的是长文导航的两个高频动作:读完想回顶部看目录、想直接跳到文末的评论区。往下滚动本页,右下角出现的那对圆形浮钮就是它的现场演示——本站开箱内置。
零依赖实现:passive 滚动监听 + rAF 节流,显隐语义经过仔细取舍(短文档两钮都不显示、到底后到底钮自动消失),移动端只保留回顶钮并加大到 44px 触摸目标。本文逐项拆解全部 props、CSS 变量与这些设计取舍。
「回到顶部」几乎是所有博客的标配,但细节做好并不容易:
scroll 回调在低端机上肉眼可见掉帧本包把这四件事都处理掉了,且不引任何滚动库,peer 依赖只有 vue ^3.5.0。
pnpm add -D @vaultkit/vitepress-plugin-scroll-navigator使用 @vaultkit/vitepress-theme-vault 主题则开箱即带(features.scrollNavigator 开关控制)。
推荐挂载在 VitePress 布局的 #layout-bottom 插槽:
<script setup lang="ts">
import { ScrollNavigator } from '@vaultkit/vitepress-plugin-scroll-navigator'
import DefaultTheme from 'vitepress/theme'
</script>
<template>
<DefaultTheme.Layout>
<template #layout-bottom>
<ScrollNavigator :labels="{ top: '回到顶部', bottom: '滚动到底部' }" />
</template>
</DefaultTheme.Layout>
</template>本包发布的是含 .vue 源文件的产物,消费方 VitePress 项目必须把它加进 vite.ssr.noExternal,否则 SSR 构建会报错:
// .vitepress/config.ts
import { defineConfig } from 'vitepress'
export default defineConfig({
vite: { ssr: { noExternal: ['@vaultkit/vitepress-plugin-scroll-navigator'] } },
})| 导出 | 形态 | 说明 |
|---|---|---|
ScrollNavigator | Vue 组件(具名导出) | 浮钮组本体 |
| 默认导出 | 同上 | import ScrollNavigator from '...' 亦可 |
只有两个 props,全部可选。
number,默认 200<ScrollNavigator :threshold="600" />注意该阈值只管回顶钮。到底钮的显隐是另一套独立语义:已滚动(scrollY > 0)且未到达底部才显示,所以:
这套组合逻辑保证按钮只在「真的有用」的时刻出现。
{ top?: string, bottom?: string },默认 {}title 与 aria-label 文案注入,逐字段说明:| 字段 | 默认值 | 对应控件 |
|---|---|---|
top | 'Back to top' | 回顶钮(向上箭头) |
bottom | 'Scroll to bottom' | 到底钮(向下箭头) |
中文站点的注入示例:
<ScrollNavigator
:labels="{
top: '回到顶部',
bottom: '滚动到底部',
}"
/>文案同时写入 title(鼠标悬停提示)与 aria-label(屏幕阅读器播报),一处注入两处生效。按钮内部只有装饰性 SVG 箭头(aria-hidden="true"),可访问名完全由 aria-label 提供——不注入中文的话,读屏用户听到的是英文。
| 变量 | 默认值 | 说明 |
|---|---|---|
--scroll-nav-right | 24px | 按钮组距视口右缘距离 |
--scroll-nav-bottom | 24px | 按钮组距视口底缘距离 |
按钮组是 position: fixed 的纵向 flex 列(间距 8px),位置经这两个变量覆盖。典型场景:站点右下角已有客服浮窗,把按钮组往上挪让位:
/* .vitepress/theme/custom.css */
:root {
--scroll-nav-bottom: 96px;
}按钮本身的配色全部走 VitePress 语义变量(--vp-c-bg-soft 底、--vp-c-divider 边、悬停变 --vp-c-brand-1),明暗两态免配置;还带 backdrop-filter: blur(8px) 毛玻璃,压在代码块上也不突兀。
和姊妹组件 reading-progress:顶部阅读进度条 同款方案:scroll / resize 回调只负责 requestAnimationFrame(update) 排程,每帧至多读一次 scrollY / scrollHeight,滚动零卡顿。
SSR 处理则更进一步:组件在 mounted 前整体不渲染(v-if="mounted")。因为服务端不知道滚动位置,任何初始态都可能错;干脆首帧空渲染,等客户端挂载后再按真实位置显示。这等效于 VitePress 的 <ClientOnly>,但自实现后不依赖 VitePress 全局组件——组件因此框架无关,任何 Vue 3 SSR 站点可用。
判断「未到底」用了 y + innerHeight < scrollHeight - 1,留 1px 容差——高 DPI 屏幕上滚动位置常有亚像素误差,不留容差按钮会在底部反复闪。
点击行为就是一行 window.scrollTo({ top, behavior: 'smooth' })。不自己写缓动动画有个关键收益:浏览器在 prefers-reduced-motion: reduce 下会自动把平滑滚动退化为瞬时跳转,无障碍偏好免费获得。按钮出现 / 消失的 Vue Transition(淡入 + 10px 上移)也在同一媒体查询下关闭。
≤768px 时组件做两件事:
@media (max-width: 768px) {
.scroll-nav-btn {
width: 44px;
height: 44px;
}
.scroll-nav-btn.is-bottom {
display: none;
}
}为什么加大到 44px:Apple HIG 建议触摸目标最小 44×44pt,桌面端的 40px 圆钮在手机上处于误触边缘。
为什么砍掉到底钮:这是一个刻意的取舍——回顶是移动端最痛的操作(iOS 有点状态栏回顶手势,Android 没有原生等价物),而「到底部」价值低得多;两钮一起显示会翻倍遮挡面积,在小屏上遮的还是正文。保回顶、砍到底,是主流博客的共同折衷。
:deep(.scroll-navigator) { z-index: ... } 或全局样式覆盖window,正文在自定义滚动容器内的布局不适用#layout-top 放 reading-progress:顶部阅读进度条、#layout-bottom 放本组件,是「滚动体验二件套」的推荐布线(本站即如此)appear 过渡:Transition 带 appear,从页面中部刷新恢复滚动位置时,按钮也是淡入而非瞬现Q:按钮一直不出现? 页面可能太短(不可滚),或者滚动发生在自定义容器而非 window 上。
Q:想只要回顶钮、不要到底钮? 组件没有单独开关,但一行全局 CSS 即可:.scroll-nav-btn.is-bottom { display: none }(组件在移动端就是这么做的)。
Q:点击回顶后焦点在哪里? 仍在按钮上。键盘用户随后 Tab 会从按钮继续,如需「回顶 + 焦点复位」的完整无障碍链路,可在使用侧监听点击后手动 focus 页面主标题。
Q:SSR 构建报 .vue 无法解析? 没配 vite.ssr.noExternal,见上文安装一节。