ScrollTop 返回顶部

滚动超过阈值后出现,返回页面或指定容器的顶部。

在面板内向下滚动,超过 120px 后显示回顶按钮。

明确操作的主次

一个区域通常只需要一个主要操作。次要操作放在内容附近,低频操作可以收到菜单里,让常用路径保持清晰。

给出可见的反馈

保存、上传和同步都需要反馈。等待时保留内容,完成后给出结果;失败时保留用户输入,让用户能够直接重试。

让长内容易于浏览

用标题建立结构,用间距区分段落。列表很长时保留稳定的滚动位置,提供返回开头的入口,而不是在内容更新时强制滚动。

照顾不同输入方式

鼠标、触摸和键盘都应当能完成操作。焦点清晰可见,图标按钮有明确名称,浮层关闭后将焦点返回合理的位置。

减少不必要的动态效果

动效用来解释状态和位置的变化。系统开启减少动态效果时,保留操作结果,让用户不必等待滚动或移动动画。

最后检查真实场景

换一组更长的文案,缩窄窗口,试用键盘,再检查深色模式。组件只有在这些场景里都能工作,才算完成。

<script setup lang="ts">
  import { ref } from 'vue'
  import { Card, Heading, ScrollArea, ScrollTop, Stack, Text } from '@hina-ui/vue'

  const area = ref<InstanceType<typeof ScrollArea>>()
  const sections = [
    {
      title: '明确操作的主次',
      body: '一个区域通常只需要一个主要操作。次要操作放在内容附近,低频操作可以收到菜单里,让常用路径保持清晰。',
    },
    {
      title: '给出可见的反馈',
      body: '保存、上传和同步都需要反馈。等待时保留内容,完成后给出结果;失败时保留用户输入,让用户能够直接重试。',
    },
    {
      title: '让长内容易于浏览',
      body: '用标题建立结构,用间距区分段落。列表很长时保留稳定的滚动位置,提供返回开头的入口,而不是在内容更新时强制滚动。',
    },
    {
      title: '照顾不同输入方式',
      body: '鼠标、触摸和键盘都应当能完成操作。焦点清晰可见,图标按钮有明确名称,浮层关闭后将焦点返回合理的位置。',
    },
    {
      title: '减少不必要的动态效果',
      body: '动效用来解释状态和位置的变化。系统开启减少动态效果时,保留操作结果,让用户不必等待滚动或移动动画。',
    },
    {
      title: '最后检查真实场景',
      body: '换一组更长的文案,缩窄窗口,试用键盘,再检查深色模式。组件只有在这些场景里都能工作,才算完成。',
    },
  ]
</script>

<template>
  <Stack class="w-full max-w-lg" gap="sm">
    <Text size="sm" tone="muted">在面板内向下滚动,超过 120px 后显示回顶按钮。</Text>
    <Card :padded="false" class="relative overflow-hidden">
      <ScrollArea ref="area" class="h-72" :shadow="false" focusable label="交互设计笔记">
        <Stack gap="lg" class="p-5 pb-24">
          <Stack v-for="section in sections" :key="section.title" gap="sm">
            <Heading :level="3" size="base">{{ section.title }}</Heading>
            <Text size="sm" tone="muted">{{ section.body }}</Text>
          </Stack>
        </Stack>
      </ScrollArea>
      <ScrollTop :target="() => area?.viewport" :threshold="120" position="absolute" :offset="16" />
    </Card>
  </Stack>
</template>
vue

用法

import { ScrollTop } from '@hina-ui/vue'
ts

不传 target 时监听页面 window,默认滚动超过 300px 后显示。点击只改变纵向位置,保留横向位置。

<ScrollTop />
vue

页面由 AppShell、ScrollArea 或 Dialog 内部容器滚动时,传入它们暴露的 viewport。滚动目标与按钮定位是独立的:target 决定滚动哪个元素,position 决定按钮摆在哪里。

<Card class="relative" :padded="false">
  <ScrollArea ref="area" class="h-72">…</ScrollArea>
  <ScrollTop :target="() => area?.viewport" position="absolute" />
</Card>
vue

AppShell 对应 () => shell?.mainViewport。默认固定定位时,将按钮放在应用壳外层;局部使用则放在滚动区域的外面、定位容器里面,避免按钮跟着内容一起滚走。

示例

避开底部内容

页面底部有固定的页脚或工具栏时,用 offset 的 y 抬高按钮,水平边距保持默认。字符串可以写任意 CSS 长度。

<ScrollTop :offset="{ y: 96 }" />
<ScrollTop :offset="{ y: 'calc(var(--footer-height) + 1rem)' }" />
vue

外观、滚动方式与焦点

沿用 FloatButton 的尺寸、形状、文字和定位。默认平滑滚动;behavior="instant" 立即定位。系统开启减少动态效果时,总是立即定位。

默认保留键盘用户的按钮焦点,直到焦点离开才隐藏。也可以通过 focus-target 将焦点明确移到顶部标题;目标需可聚焦,例如设置 tabindex="-1" 的标题。

发布检查项

验证核心流程

检查空状态

检查加载和重试

检查键盘操作

检查表单错误

检查移动端排版

检查深色模式

检查 RTL

检查服务端渲染

核对文档示例

补齐变更记录

确认发布版本

回顶后将焦点移到标题,可以从这里继续键盘浏览。

<script setup lang="ts">
  import { ref } from 'vue'
  import { ChevronsUp } from '@lucide/vue'
  import {
    Button,
    Card,
    FormField,
    Heading,
    ScrollArea,
    ScrollTop,
    Stack,
    Switch,
    Text,
  } from '@hina-ui/vue'

  const area = ref<InstanceType<typeof ScrollArea>>()
  const heading = ref<InstanceType<typeof Heading>>()
  const instant = ref(false)
  function goDown() {
    area.value?.viewport?.scrollTo({ top: 450, behavior: 'instant' })
  }
</script>

<template>
  <Stack class="w-full max-w-md" gap="sm">
    <FormField label="立即回顶" orientation="horizontal"><Switch v-model="instant" /></FormField>
    <Button variant="outline" tone="neutral" class="self-start" @click="goDown">滚动到中段</Button>
    <Card :padded="false" class="relative overflow-hidden">
      <ScrollArea ref="area" class="h-64" :shadow="false" focusable label="发布检查项">
        <Stack class="p-5 pb-24" gap="lg">
          <Heading
            ref="heading"
            :level="3"
            size="base"
            tabindex="-1"
            class="hn-focus-ring rounded-sm"
          >
            发布检查项
          </Heading>
          <Text
            v-for="item in [
              '验证核心流程',
              '检查空状态',
              '检查加载和重试',
              '检查键盘操作',
              '检查表单错误',
              '检查移动端排版',
              '检查深色模式',
              '检查 RTL',
              '检查服务端渲染',
              '核对文档示例',
              '补齐变更记录',
              '确认发布版本',
            ]"
            :key="item"
            size="sm"
            class="border-line border-b pb-3"
          >
            {{ item }}
          </Text>
        </Stack>
      </ScrollArea>
      <ScrollTop
        :target="() => area?.viewport"
        :focus-target="() => heading?.$el"
        :threshold="80"
        :behavior="instant ? 'instant' : 'smooth'"
        position="absolute"
        :offset="16"
        extended
        size="sm"
        shape="square"
        variant="soft"
        tone="accent"
        label="返回检查项开头"
      >
        <ChevronsUp />
      </ScrollTop>
    </Card>
    <Text size="sm" tone="muted">回顶后将焦点移到标题,可以从这里继续键盘浏览。</Text>
  </Stack>
</template>
vue

延迟挂载的目标

传入 getter 可以跟随 ScrollArea 的初始化、卸载和替换。getter 返回空值时按钮隐藏,不会回退到页面滚动。切换目标时旧监听器会被移除,新目标立即按自己的滚动位置决定显隐。

审阅记录 1 · 检查交互与视觉细节

审阅记录 2 · 检查交互与视觉细节

审阅记录 3 · 检查交互与视觉细节

审阅记录 4 · 检查交互与视觉细节

审阅记录 5 · 检查交互与视觉细节

审阅记录 6 · 检查交互与视觉细节

审阅记录 7 · 检查交互与视觉细节

审阅记录 8 · 检查交互与视觉细节

审阅记录 9 · 检查交互与视觉细节

审阅记录 10 · 检查交互与视觉细节

审阅记录 11 · 检查交互与视觉细节

审阅记录 12 · 检查交互与视觉细节

审阅记录 13 · 检查交互与视觉细节

审阅记录 14 · 检查交互与视觉细节

审阅记录 15 · 检查交互与视觉细节

审阅记录 16 · 检查交互与视觉细节

审阅记录 17 · 检查交互与视觉细节

审阅记录 18 · 检查交互与视觉细节

审阅记录 19 · 检查交互与视觉细节

审阅记录 20 · 检查交互与视觉细节

回顶组件保持挂载。目标尚未准备好或已卸载时,按钮隐藏,也不会改为滚动页面。

<script setup lang="ts">
  import { ref } from 'vue'
  import { Button, Card, FormField, ScrollArea, ScrollTop, Stack, Switch, Text } from '@hina-ui/vue'

  const area = ref<InstanceType<typeof ScrollArea>>()
  const show = ref(true)
</script>

<template>
  <Stack class="w-full max-w-md" gap="sm">
    <FormField label="挂载滚动区域" orientation="horizontal"><Switch v-model="show" /></FormField>
    <Button
      variant="outline"
      tone="neutral"
      class="self-start"
      :disabled="!show"
      @click="area?.viewport?.scrollTo({ top: 500, behavior: 'instant' })"
    >
      滚动到中段
    </Button>
    <Card :padded="false" class="relative h-64 overflow-hidden">
      <ScrollArea v-if="show" ref="area" class="h-full" :shadow="false" focusable label="审阅记录">
        <Stack class="p-5 pb-24" gap="lg">
          <Text v-for="i in 20" :key="i" size="sm" class="border-line border-b pb-3">
            审阅记录 {{ i }} · 检查交互与视觉细节
          </Text>
        </Stack>
      </ScrollArea>
      <Text v-else size="sm" tone="muted" class="p-5">滚动区域已卸载</Text>
      <ScrollTop :target="() => area?.viewport" position="absolute" :threshold="120" :offset="16" />
    </Card>
    <Text size="sm" tone="muted">
      回顶组件保持挂载。目标尚未准备好或已卸载时,按钮隐藏,也不会改为滚动页面。
    </Text>
  </Stack>
</template>
vue

行为

  • 只监听目标的被动 scroll 事件,不逐帧轮询或扫描内容。
  • 服务端不读取目标 getter,也不渲染回顶按钮;挂载后读取真实滚动位置。
  • 按钮在纵向滚动位置严格大于 threshold 时显示。数值变化可以动态生效。
  • loading / disabled 阻止点击和暴露方法触发滚动。
  • @click.prevent 可取消默认回顶行为。click 表示操作被触发,不代表平滑滚动已经结束。

API

Props

属性
类型
默认值
说明
target
HTMLElement | Window | null | (() => HTMLElement | Window | null | undefined)
window
滚动目标;传 getter 等待异步容器
threshold
number
300
按钮显示的纵向阈值,单位 px
behavior
'smooth' | 'instant' | 'auto'
'smooth'
滚动方式;auto 遵循目标的 CSS 滚动行为
focusTarget
HTMLElement | (() => HTMLElement | null | undefined)
—
激活时聚焦此节点,使用 preventScroll 避免额外跳动
label
string
当前语言的“返回顶部”
按钮名称
variant
'solid' | 'soft' | 'outline'
'outline'
按钮外观
tone
'accent' | 'neutral' | 'danger'
'neutral'
按钮色调

另外支持 FloatButton 的 position、placement、offset、size、shape、extended、tooltip、tooltipSide、loading、disabled、ripple、class 和 style,默认值相同。显隐由滚动状态决定,不接收 visible。

Slots

插槽默认内容
说明
default向上箭头
替换按钮图标

Events

事件
参数
说明
click
MouseEvent
点击回顶按钮;可阻止默认行为

Expose

名称
类型
说明
visible
boolean
当前显示状态(只读)
scrollToTop
() => void
使用当前配置滚动到目标顶部