ScrollTop 返回顶部

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

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

明确操作的主次

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

给出可见的反馈

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

让长内容易于浏览

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

照顾不同输入方式

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

减少不必要的动态效果

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

最后检查真实场景

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

'use client'

import { useRef } from 'react'
import {
  Card,
  Heading,
  ScrollArea,
  ScrollTop,
  Stack,
  Text,
  type ScrollAreaHandle,
} from '@hina-ui/react'

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

export default function Demo() {
  const area = useRef<ScrollAreaHandle>(null)

  return (
    <Stack className="w-full max-w-lg" gap="sm">
      <Text size="sm" tone="muted">
        在面板内向下滚动,超过 120px 后显示回顶按钮。
      </Text>
      <Card padded={false} className="relative overflow-hidden">
        <ScrollArea ref={area} className="h-72" shadow={false} focusable label="交互设计笔记">
          <Stack gap="lg" className="p-5 pb-24">
            {sections.map(section => (
              <Stack 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.current?.viewport}
          threshold={120}
          position="absolute"
          offset={16}
        />
      </Card>
    </Stack>
  )
}
tsx

用法

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

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

<ScrollTop />
tsx

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

'use client'

import { useRef } from 'react'
import { Card, ScrollArea, ScrollTop, type ScrollAreaHandle } from '@hina-ui/react'

export function History() {
  const area = useRef<ScrollAreaHandle>(null)

  return (
    <Card className="relative" padded={false}>
      <ScrollArea ref={area} className="h-72">
        …
      </ScrollArea>
      <ScrollTop target={() => area.current?.viewport} position="absolute" />
    </Card>
  )
}
tsx

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

示例

外观、滚动方式与焦点

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

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

发布检查项

验证核心流程

检查空状态

检查加载和重试

检查键盘操作

检查表单错误

检查移动端排版

检查深色模式

检查 RTL

检查服务端渲染

核对文档示例

补齐变更记录

确认发布版本

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

'use client'

import { useRef, useState } from 'react'
import { ChevronsUp } from 'lucide-react'
import {
  Button,
  Card,
  FormField,
  Heading,
  ScrollArea,
  ScrollTop,
  Stack,
  Switch,
  Text,
  type ScrollAreaHandle,
} from '@hina-ui/react'

export default function Demo() {
  const area = useRef<ScrollAreaHandle>(null)
  const heading = useRef<HTMLHeadingElement>(null)
  const [instant, setInstant] = useState(false)
  function goDown() {
    area.current?.viewport?.scrollTo({ top: 450, behavior: 'instant' })
  }

  return (
    <Stack className="w-full max-w-md" gap="sm">
      <FormField label="立即回顶" orientation="horizontal">
        <Switch checked={instant} onCheckedChange={setInstant} />
      </FormField>
      <Button variant="outline" tone="neutral" className="self-start" onClick={goDown}>
        滚动到中段
      </Button>
      <Card padded={false} className="relative overflow-hidden">
        <ScrollArea ref={area} className="h-64" shadow={false} focusable label="发布检查项">
          <Stack className="p-5 pb-24" gap="lg">
            <Heading
              ref={heading}
              level={3}
              size="base"
              tabIndex={-1}
              className="hn-focus-ring rounded-sm"
            >
              发布检查项
            </Heading>
            {[
              '验证核心流程',
              '检查空状态',
              '检查加载和重试',
              '检查键盘操作',
              '检查表单错误',
              '检查移动端排版',
              '检查深色模式',
              '检查 RTL',
              '检查服务端渲染',
              '核对文档示例',
              '补齐变更记录',
              '确认发布版本',
            ].map(item => (
              <Text key={item} size="sm" className="border-line border-b pb-3">
                {item}
              </Text>
            ))}
          </Stack>
        </ScrollArea>
        <ScrollTop
          target={() => area.current?.viewport}
          focusTarget={() => heading.current}
          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>
  )
}
tsx

延迟挂载的目标

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

'use client'

import { useRef, useState } from 'react'
import {
  Button,
  Card,
  FormField,
  ScrollArea,
  ScrollTop,
  Stack,
  Switch,
  Text,
  type ScrollAreaHandle,
} from '@hina-ui/react'

export default function Demo() {
  const area = useRef<ScrollAreaHandle>(null)
  const [show, setShow] = useState(true)

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

行为

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

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、className 和 style,默认值相同。显隐由滚动状态决定,不接收 visible。

内容属性

属性默认内容
说明
children向上箭头
替换按钮图标

回调

回调
参数
说明
onClick
MouseEvent
点击回顶按钮;可阻止默认行为

Ref

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