Overview
Scroll this area and the entries on the right follow along.
Installation
Scroll this area and the entries on the right follow along.
Usage
Scroll this area and the entries on the right follow along.
<script setup lang="ts">
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/vue'
const sections = [
{ id: 'hero-intro', label: 'Overview' },
{ id: 'hero-install', label: 'Installation' },
{ id: 'hero-usage', label: 'Usage' },
]
</script>
<template>
<Inline gap="lg" align="start" class="w-full max-w-2xl">
<ScrollArea class="border-line h-56 flex-1 rounded-lg border">
<Section v-for="s in sections" :id="s.id" :key="s.id" class="min-h-40 p-4">
<Heading :level="3" size="sm">{{ s.label }}</Heading>
<Text size="sm" tone="muted">
Scroll this area and the entries on the right follow along.
</Text>
</Section>
</ScrollArea>
<Anchor :items="sections" class="w-28 shrink-0" />
</Inline>
</template>
Usage
import { Anchor } from '@hina-ui/vue'
Anchor is driven by items. Each entry’s id matches the id of an element on the page, and label is the text shown in the contents.
It renders the contents, not the content. Every id must resolve to an element in the document; an entry whose target is missing is neither tracked nor clickable.
Synopsis
Body text for this section.
Characters
Body text for this section.
Staff
Body text for this section.
<script setup lang="ts">
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/vue'
const items = [
{ id: 'basic-a', label: 'Synopsis' },
{ id: 'basic-b', label: 'Characters' },
{ id: 'basic-c', label: 'Staff' },
]
</script>
<template>
<Inline gap="lg" align="start" class="w-full max-w-2xl">
<ScrollArea class="border-line h-56 flex-1 rounded-lg border">
<Section v-for="item in items" :id="item.id" :key="item.id" class="min-h-40 p-4">
<Heading :level="3" size="sm">{{ item.label }}</Heading>
<Text size="sm" tone="muted">Body text for this section.</Text>
</Section>
</ScrollArea>
<Anchor :items="items" class="w-28 shrink-0" />
</Inline>
</template>
Tracking is based on an element’s visibility in the viewport, so the content works equally well in the page’s main scroll region or inside a ScrollArea.
Examples
Levels
children adds a second level, shown one indent in. The contents support two levels only — a children array nested inside children is not rendered. An in-page contents list deeper than two levels is a sign the page should be split rather than nested further.
Overview
Body text for this section.
Routes
Body text for this section.
Common route
Body text for this section.
Character routes
Body text for this section.
Staff
Body text for this section.
<script setup lang="ts">
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/vue'
const items = [
{ id: 'nest-intro', label: 'Overview' },
{
id: 'nest-route',
label: 'Routes',
children: [
{ id: 'nest-route-a', label: 'Common route' },
{ id: 'nest-route-b', label: 'Character routes' },
],
},
{ id: 'nest-staff', label: 'Staff' },
]
const flat = items.flatMap(item => [item, ...(item.children ?? [])])
</script>
<template>
<Inline gap="lg" align="start" class="w-full max-w-2xl">
<ScrollArea class="border-line h-56 flex-1 rounded-lg border">
<Section v-for="item in flat" :id="item.id" :key="item.id" class="min-h-32 p-4">
<Heading :level="3" size="sm">{{ item.label }}</Heading>
<Text size="sm" tone="muted">Body text for this section.</Text>
</Section>
</ScrollArea>
<Anchor :items="items" class="w-32 shrink-0" />
</Inline>
</template>
Trailing content
#trailing receives { item, active } for non-interactive content such as a Tag, text or an icon. item is the original entry, including custom fields. Both top-level and second-level entries support the slot.
active matches aria-current="location". When several sections are visible, only the first visible entry in directory order is active; the existing visible-range highlight is unchanged.
Trailing content aligns to the end of the row without shrinking, while long labels can wrap. Empty trailing content takes no space. Updating a status mark keeps the current entry, and clicking the mark still follows its link.
Entry A
The content of this section.
Entry B
The content of this section.
Child entry
The content of this section.
<script setup lang="ts">
import { computed, ref } from 'vue'
import {
Anchor,
Button,
Heading,
Inline,
ScrollArea,
Section,
Stack,
Tag,
Text,
} from '@hina-ui/vue'
const modified = ref(true)
const items = computed(() => [
{ id: 'trailing-a', label: 'Entry A', modified: modified.value },
{
id: 'trailing-b',
label: 'Entry B',
modified: false,
children: [{ id: 'trailing-c', label: 'Child entry', modified: true }],
},
])
const sections = computed(() => items.value.flatMap(item => [item, ...(item.children ?? [])]))
</script>
<template>
<Stack gap="md" class="w-full max-w-2xl">
<Inline>
<Button variant="soft" tone="neutral" :aria-pressed="modified" @click="modified = !modified">
Toggle the mark on entry A
</Button>
</Inline>
<Inline gap="lg" align="start" :wrap="false">
<ScrollArea class="border-line h-56 min-w-0 flex-1 rounded-lg border">
<Section v-for="item in sections" :id="item.id" :key="item.id" class="min-h-40 p-4">
<Heading :level="3" size="sm">{{ item.label }}</Heading>
<Text size="sm" tone="muted">The content of this section.</Text>
</Section>
</ScrollArea>
<Anchor :items="items" label="Contents with trailing marks" class="w-44 shrink-0">
<template #trailing="{ item, active }">
<Tag v-if="item.modified" :tone="active ? 'accent' : 'neutral'">Modified</Tag>
</template>
</Anchor>
</Inline>
</Stack>
</template>
Following a long directory
Place a long directory inside a height-constrained ScrollArea. When the highlighted range extends beyond the directory viewport, Anchor scrolls the entire range into view with a small margin. If the range is taller than the viewport, it follows the first current entry instead of alternating between the two ends. It detects the nearest vertical scroll container, including native containers or a scrollable root element, without requiring a viewport ref.
Fully visible ranges are not repeatedly centered, and browsing the directory manually does not continually pull it back. Initial positioning and size adjustments are instant; subsequent changes to either end of the highlighted range scroll smoothly unless reduced motion is enabled. Following never moves focus or scrolls an outer container shared with the article.
Set :auto-scroll="false" to disable following while keeping scroll-spy and highlighting. @change receives the current entry’s id, and a component ref exposes the read-only current value, so observing aria-current is unnecessary. This example contains 51 entries; scroll the article and directory independently or disable following to compare.
51 sections
1. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
2. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
3. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
4. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
5. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
6. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
7. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
8. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
9. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
10. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
11. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
12. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
13. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
14. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
15. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
16. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
17. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
18. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
19. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
20. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
21. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
22. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
23. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
24. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
25. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
26. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
27. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
28. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
29. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
30. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
31. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
32. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
33. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
34. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
35. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
36. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
37. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
38. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
39. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
40. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
41. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
42. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
43. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
44. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
45. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
46. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
47. Design process
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
48. Implementation
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
49. User experience
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
50. Conclusions
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
51. Background
Scroll through the article to keep its current entry visible in the contents. You can also browse the contents independently to find another section.
Current: Article is outside the reading area
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Anchor, Heading, Inline, ScrollArea, Section, Stack, Switch, Text } from '@hina-ui/vue'
const topics = [
'Background',
'Design process',
'Implementation',
'User experience',
'Conclusions',
]
const items = Array.from({ length: 51 }, (_, index) => ({
id: `long-toc-${index + 1}`,
label: `${index + 1}. ${topics[index % topics.length]}`,
}))
const autoScroll = ref(true)
const current = ref<string>()
const currentLabel = computed(() => items.find(item => item.id === current.value)?.label)
</script>
<template>
<Stack class="w-full max-w-2xl">
<Inline justify="between">
<Switch v-model="autoScroll">Follow current entry</Switch>
<Text size="sm" tone="muted">51 sections</Text>
</Inline>
<Inline align="start" gap="md" :wrap="false">
<ScrollArea
class="border-line h-80 min-w-0 flex-1 rounded-lg border"
focusable
label="Article content"
>
<Section v-for="item in items" :id="item.id" :key="item.id" class="min-h-56 p-4">
<Heading :level="3" size="sm">{{ item.label }}</Heading>
<Text size="sm" tone="muted">
Scroll through the article to keep its current entry visible in the contents. You can
also browse the contents independently to find another section.
</Text>
</Section>
</ScrollArea>
<ScrollArea
class="h-80 w-2/5 max-w-48 shrink-0"
:shadow="false"
focusable
label="Scrollable article contents"
>
<Anchor
:items="items"
:auto-scroll="autoScroll"
label="Long article contents"
@change="current = $event"
/>
</ScrollArea>
</Inline>
<Text size="sm" tone="muted">
Current: {{ currentLabel ?? 'Article is outside the reading area' }}
</Text>
</Stack>
</template>
Landmark name
Anchor renders as a nav landmark with an accessible name in the interface language (“On this page” in English). When a page holds several navigation landmarks, name each with label.
Version 1.2
What changed in this version.
Version 1.1
What changed in this version.
Version 1.0
What changed in this version.
<script setup lang="ts">
import { Anchor, Heading, Inline, ScrollArea, Section, Text } from '@hina-ui/vue'
const items = [
{ id: 'label-a', label: 'Version 1.2' },
{ id: 'label-b', label: 'Version 1.1' },
{ id: 'label-c', label: 'Version 1.0' },
]
</script>
<template>
<Inline gap="lg" align="start" class="w-full max-w-2xl">
<ScrollArea class="border-line h-56 flex-1 rounded-lg border">
<Section v-for="item in items" :id="item.id" :key="item.id" class="min-h-40 p-4">
<Heading :level="3" size="sm">{{ item.label }}</Heading>
<Text size="sm" tone="muted">What changed in this version.</Text>
</Section>
</ScrollArea>
<Anchor :items="items" label="Version contents" class="w-28 shrink-0" />
</Inline>
</template>
Behaviour
- When several sections are visible at once they are all marked, and the highlight bar stretches across the whole run rather than picking one out.
- The bar moves continuously rather than jumping.
- Roughly the bottom 15% of the viewport does not count as visible, so a section is marked only once it has properly entered the reading area.
- Clicking an entry scrolls smoothly to the target and replaces the hash in the address bar, but adds no history entry.
- With reduced motion enabled the jump is instant rather than smooth.
Accessibility
- The wrapper is a
navlandmark holding a list of links. - The current entry carries
aria-current="location"—locationrather thanpage, because the reader is still on the same page and only the position within it has changed. - Entries are real links whose
hrefpoints at the anchor, so they can be copied or opened in a new tab. - The highlight bar is decoration; position is carried by
aria-currentand the heavier text together, never by colour alone.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | T[] | Required | Entries of the contents list |
label | string | Interface language | Accessible name of the landmark |
autoScroll | boolean | true | Follow the highlighted range, or its first entry when too tall |
class | string | — | Classes appended to the root |
AnchorItem
| Field | Type | Description |
|---|---|---|
id | string | id of the target element, without the # |
label | string | Text shown in the contents |
children | AnchorItem[] | Second-level entries; their own children ignored |
Slots
| Slot | Parameters | Description |
|---|---|---|
trailing | { item, active: boolean } | Trailing content inside each entry link |
T is inferred from items and must include the base fields of AnchorItem. item preserves the original types of top-level entries and their children; use a field or discriminant to narrow the type when parent and child fields differ.
Events
| Event | Payload | Description |
|---|---|---|
change | current: string | undefined | Emitted when the current entry changes, including its first detection. Becomes undefined when resetting entries leaves no current entry. |
Expose
| Property | Type | Description |
|---|---|---|
current | string | undefined | Read-only current entry id; undefined until an entry is identified |