简介
滚动此区域,右侧条目随之更新。
安装
滚动此区域,右侧条目随之更新。
用法
滚动此区域,右侧条目随之更新。
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/react'
const sections = [
{ id: 'hero-intro', label: '简介' },
{ id: 'hero-install', label: '安装' },
{ id: 'hero-usage', label: '用法' },
]
export default function Demo() {
return (
<Inline gap="lg" align="start" className="w-full max-w-2xl">
<ScrollArea className="border-line h-56 flex-1 rounded-lg border">
{sections.map(s => (
<Section id={s.id} key={s.id} className="min-h-40 p-4">
<Heading level={3} size="sm">
{s.label}
</Heading>
<Text size="sm" tone="muted">
滚动此区域,右侧条目随之更新。
</Text>
</Section>
))}
</ScrollArea>
<Anchor items={sections} className="w-28 shrink-0" />
</Inline>
)
}
用法
import { Anchor } from '@hina-ui/react'
Anchor 由 items 驱动,每一项的 id 对应页面中一个元素的 id,label 是目录上显示的文字。
它只渲染目录,不渲染内容。每个 id 必须能在文档中找到对应元素,否则该项既不会被跟踪,点击也不会有反应。
作品简介
这一节的正文。
登场角色
这一节的正文。
制作人员
这一节的正文。
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/react'
const items = [
{ id: 'basic-a', label: '作品简介' },
{ id: 'basic-b', label: '登场角色' },
{ id: 'basic-c', label: '制作人员' },
]
export default function Demo() {
return (
<Inline gap="lg" align="start" className="w-full max-w-2xl">
<ScrollArea className="border-line h-56 flex-1 rounded-lg border">
{items.map(item => (
<Section id={item.id} key={item.id} className="min-h-40 p-4">
<Heading level={3} size="sm">
{item.label}
</Heading>
<Text size="sm" tone="muted">
这一节的正文。
</Text>
</Section>
))}
</ScrollArea>
<Anchor items={items} className="w-28 shrink-0" />
</Inline>
)
}
它跟踪的是元素在视口中的可见性,因此内容位于页面主滚动区或 ScrollArea 之中,均可正常跟踪。
示例
层级
children 提供第二层条目,缩进一级显示。目录只支持两层——children 里再写 children 不会渲染。页内目录超过两层时,应当考虑拆分页面,而不是继续加深层级。
简介
这一节的正文。
路线
这一节的正文。
共通线
这一节的正文。
个人线
这一节的正文。
制作人员
这一节的正文。
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/react'
const items = [
{ id: 'nest-intro', label: '简介' },
{
id: 'nest-route',
label: '路线',
children: [
{ id: 'nest-route-a', label: '共通线' },
{ id: 'nest-route-b', label: '个人线' },
],
},
{ id: 'nest-staff', label: '制作人员' },
]
const flat = items.flatMap(item => [item, ...(item.children ?? [])])
export default function Demo() {
return (
<Inline gap="lg" align="start" className="w-full max-w-2xl">
<ScrollArea className="border-line h-56 flex-1 rounded-lg border">
{flat.map(item => (
<Section id={item.id} key={item.id} className="min-h-32 p-4">
<Heading level={3} size="sm">
{item.label}
</Heading>
<Text size="sm" tone="muted">
这一节的正文。
</Text>
</Section>
))}
</ScrollArea>
<Anchor items={items} className="w-32 shrink-0" />
</Inline>
)
}
尾部内容
renderTrailing 接收 { item, active },可放置 Tag、文字或图标等非交互内容。item 是原始条目,包含自定义字段;顶层与第二层条目均支持。
active 表示该项带有 aria-current="location"。多个小节同时可见时,仅目录顺序中的第一个可见项为 active,原有的可见区间高亮保持不变。
尾部靠行尾排列,不压缩自身宽度,长标题可换行。空的尾部不占位;更新状态标记不会重置当前项,点击标记仍由该行执行跳转。
条目 A
这一节的正文。
条目 B
这一节的正文。
子条目
这一节的正文。
'use client'
import { useState } from 'react'
import {
Anchor,
Button,
Heading,
Inline,
ScrollArea,
Section,
Stack,
Tag,
Text,
} from '@hina-ui/react'
export default function Demo() {
const [modified, setModified] = useState(true)
const items = [
{ id: 'trailing-a', label: '条目 A', modified },
{
id: 'trailing-b',
label: '条目 B',
modified: false,
children: [{ id: 'trailing-c', label: '子条目', modified: true }],
},
]
const sections = items.flatMap(item => [item, ...(item.children ?? [])])
return (
<Stack gap="md" className="w-full max-w-2xl">
<Inline>
<Button
variant="soft"
tone="neutral"
aria-pressed={modified}
onClick={() => setModified(!modified)}
>
切换条目 A 的标记
</Button>
</Inline>
<Inline gap="lg" align="start" wrap={false}>
<ScrollArea className="border-line h-56 min-w-0 flex-1 rounded-lg border">
{sections.map(item => (
<Section id={item.id} key={item.id} className="min-h-40 p-4">
<Heading level={3} size="sm">
{item.label}
</Heading>
<Text size="sm" tone="muted">
这一节的正文。
</Text>
</Section>
))}
</ScrollArea>
<Anchor
items={items}
label="带尾部标记的目录"
className="w-44 shrink-0"
renderTrailing={({ item, active }) =>
item.modified ? <Tag tone={active ? 'accent' : 'neutral'}>已修改</Tag> : null
}
/>
</Inline>
</Stack>
)
}
长目录跟随
把长目录放入限定高度的 ScrollArea,当前高亮段超出目录可视区域时,Anchor 会自动滚动目录使整段完整可见,并保留少量边距。高亮段比可视区域更高时,退回跟随段首当前项,不在首尾之间来回滚动。它会识别最近的纵向滚动容器,也支持原生滚动容器或根元素自身滚动,无需传入 viewport。
已经完整可见的高亮段不会反复居中,手动翻看目录也不会被持续拉回。首次定位和尺寸调整直接对齐,后续高亮段首尾变化时平滑跟随;减弱动态效果下始终瞬时定位。自动跟随不会移动焦点,也不会滚动同时包含正文的外层容器。
通过 autoScroll={false} 关闭自动跟随,scroll-spy 和高亮仍然工作。onChange 接收当前条目的 id,组件 ref 上也可读取只读的 current,不需要观察 aria-current。下面的目录包含 51 项,可以独立滚动正文和目录,或关闭跟随作比较。
共 51 节
第 1 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 2 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 3 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 4 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 5 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 6 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 7 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 8 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 9 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 10 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 11 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 12 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 13 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 14 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 15 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 16 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 17 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 18 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 19 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 20 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 21 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 22 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 23 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 24 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 25 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 26 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 27 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 28 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 29 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 30 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 31 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 32 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 33 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 34 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 35 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 36 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 37 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 38 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 39 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 40 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 41 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 42 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 43 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 44 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 45 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 46 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 47 节 · 设计过程
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 48 节 · 实现细节
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 49 节 · 使用体验
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 50 节 · 总结与展望
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
第 51 节 · 背景与目标
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
当前:尚未进入正文
'use client'
import { useState } from 'react'
import { Anchor, Heading, Inline, ScrollArea, Section, Stack, Switch, Text } from '@hina-ui/react'
const topics = ['背景与目标', '设计过程', '实现细节', '使用体验', '总结与展望']
const items = Array.from({ length: 51 }, (_, index) => ({
id: `long-toc-${index + 1}`,
label: `第 ${index + 1} 节 · ${topics[index % topics.length]}`,
}))
export default function Demo() {
const [autoScroll, setAutoScroll] = useState(true)
const [current, setCurrent] = useState<string>()
const currentLabel = items.find(item => item.id === current)?.label
return (
<Stack className="w-full max-w-2xl">
<Inline justify="between">
<Switch checked={autoScroll} onCheckedChange={setAutoScroll}>
目录自动跟随
</Switch>
<Text size="sm" tone="muted">
共 51 节
</Text>
</Inline>
<Inline align="start" gap="md" wrap={false}>
<ScrollArea
className="border-line h-80 min-w-0 flex-1 rounded-lg border"
focusable
label="文章正文"
>
{items.map(item => (
<Section id={item.id} key={item.id} className="min-h-56 p-4">
<Heading level={3} size="sm">
{item.label}
</Heading>
<Text size="sm" tone="muted">
滚动正文阅读后续小节,目录会让当前项保持可见。也可以独立滚动目录,查找其他章节。
</Text>
</Section>
))}
</ScrollArea>
<ScrollArea
className="h-80 w-2/5 max-w-48 shrink-0"
shadow={false}
focusable
label="文章目录滚动区域"
>
<Anchor
items={items}
autoScroll={autoScroll}
label="长篇文章目录"
onChange={setCurrent}
/>
</ScrollArea>
</Inline>
<Text size="sm" tone="muted">
当前:{currentLabel ?? '尚未进入正文'}
</Text>
</Stack>
)
}
目录名称
Anchor 渲染为 nav 地标,带有随界面语言给出的无障碍名(简体中文为「本页目录」)。一个页面里有多个导航地标时,用 label 分别命名。
版本 1.2
这一版的更新内容。
版本 1.1
这一版的更新内容。
版本 1.0
这一版的更新内容。
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/react'
const items = [
{ id: 'label-a', label: '版本 1.2' },
{ id: 'label-b', label: '版本 1.1' },
{ id: 'label-c', label: '版本 1.0' },
]
export default function Demo() {
return (
<Inline gap="lg" align="start" className="w-full max-w-2xl">
<ScrollArea className="border-line h-56 flex-1 rounded-lg border">
{items.map(item => (
<Section id={item.id} key={item.id} className="min-h-40 p-4">
<Heading level={3} size="sm">
{item.label}
</Heading>
<Text size="sm" tone="muted">
这一版的更新内容。
</Text>
</Section>
))}
</ScrollArea>
<Anchor items={items} label="版本目录" className="w-28 shrink-0" />
</Inline>
)
}
行为
- 屏幕上同时出现多个小节时,它们一并标出,左侧的高亮条延展覆盖这一整段,而不是从中选取其一。
- 高亮条的伸缩是连续位移,不是跳变。
- 视口底部约 15% 的范围不计入可见,小节须真正进入阅读区域才会被标为当前位置。
- 点击条目平滑滚动至对应位置,并替换地址栏中的锚点,但不新增历史记录。
- 系统开启减弱动态效果时改为瞬时定位,不执行平滑滚动。
无障碍
- 外层是
nav地标,内部是链接列表。 - 当前项带
aria-current="location"。这里用location而非page:读者仍在同一个页面上,变化的是页内位置。 - 条目是真链接,
href指向对应的锚点,可以复制、可以在新标签页打开。 - 高亮条是纯装饰,位置信息由
aria-current与文字加重共同承担,不依赖颜色。
API
Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
items | T[] | 必填 | 目录条目 |
label | string | 取自界面语言 | 导航地标的无障碍名 |
autoScroll | boolean | true | 跟随当前高亮段;整段过高时跟随段首 |
className | string | — | 追加到根元素的类 |
AnchorItem
| 字段 | 类型 | 说明 |
|---|---|---|
id | string | 目标元素的 id,不含 # |
label | string | 目录上显示的文字 |
children | AnchorItem[] | 第二层条目,其 children 被忽略 |
内容属性
| 属性 | 参数 | 说明 |
|---|---|---|
renderTrailing | { item, active: boolean } | 每一项链接内部的尾部内容 |
T 从 items 推导,须包含 AnchorItem 的基础字段。item 保留顶层条目及其 children 的原始类型;父子项字段不同时可按字段或判别标记缩小类型。
回调
| 回调 | 参数 | 说明 |
|---|---|---|
onChange | current: string | undefined | 当前项变化时触发,包括首次确定当前项;条目重置后没有当前项时为 undefined |
Ref
| 属性 | 类型 | 说明 |
|---|---|---|
current | string | undefined | 只读的当前条目 id,未确定当前项时为 undefined |