Back to catalog
Component detail entry
Navigation
@vean/ui
SAnchor

Anchor

Anchor belongs to the Navigation category. Start with demos and usage, then use the API tables to finish integration and styling decisions.

Use the quick jump panel to reach overview, usage, demos, and API first, then continue through the full page.

Quick jump

Related components

If you are composing interactions in the same family, these components usually pair well together.

Anchor

Overview

Anchor provides in-page navigation for long content sections and keeps the current section highlighted while scrolling.

When the active item changes while scrolling, the hash in the address bar is also synchronized. Scroll-driven updates always use history.replaceState to avoid creating excessive history entries.

If the current URL already contains a hash on initial render, Anchor will scroll to the matching section after mount. When a custom scroll container becomes available later, Anchor will re-sync once that container is ready.

SAnchor now delegates recursive item rendering to Aria AnchorCompact. The same six ui slots are available from both SAnchor and @vean/aria/anchor.

Features

  • Scroll-spy highlighting — the root observes its scroll container (window by default or a custom element via getContainer) and marks the last section that crossed the offsetTop + bounds threshold as active, with aria-current="location" on the matching link.
  • Hash synchronization — clicking a link writes the hash with history.pushState (or history.replaceState when replace is set); scroll-driven updates always replaceState; on mount the component scrolls to the hash that is already in the URL.
  • Smooth in-page scrolling — clicking a hash link scrolls the container with behavior: 'smooth' and suspends scroll-spy re-sync until the animation settles (300 ms guard).
  • Custom scroll container — getContainer returns any scrollable HTMLElement; anchor positions are measured relative to that container and re-synced when it replaces the initial window fallback.
  • Offset control — offsetTop (default 0) and targetOffset (takes precedence) shift the scroll destination so headings land below sticky headers; the value is also exposed as --vean-anchor-offset-top for the sticky root.
  • Active detection tuning — bounds (default 5) is the pixel tolerance used to decide whether a section has been passed; getCurrentAnchor can remap the resolved href before it is emitted.
  • Recursive data-driven composition — SAnchor renders nested items (arbitrary depth) through the generic Aria AnchorCompact; each level wraps its own AnchorLink, indicator, title, and nested sub list.
  • Sticky rail — sticky (default true) pins the anchor list to the top of the container with top: var(--vean-anchor-offset-top) and caps its height to the viewport.
  • Per-item and global link props — disabled and target can be set per item or globally via linkProps; an explicit item value always wins, and linkProps.href is intentionally excluded because the item’s href owns the target.
  • Eight theme colors + six sizes — color (primary…accent) and size (xs…2xl) variants from the anchorVariants scv() recipe, plus vertical / horizontal orientation.
  • Accessible by default — the root renders a <nav> landmark with a localized aria-label (overridable), disabled links carry aria-disabled and leave the tab order, and the indicator is aria-hidden.
  • Aria composition — AnchorRoot / AnchorLink / AnchorCompact / AnchorItemCompact are exported from @vean/aria/anchor for fully custom styled builds.

Usage

SAnchor now delegates recursive item rendering to Aria AnchorCompact. For unstyled, data-driven usage, import AnchorCompact from @vean/aria/anchor.

Demos

Basic

Overview

Scroll the content area to watch the active anchor update automatically.

Usage

Scroll the content area to watch the active anchor update automatically.

Installation

Scroll the content area to watch the active anchor update automatically.

Configuration

Scroll the content area to watch the active anchor update automatically.

API

Scroll the content area to watch the active anchor update automatically.

Custom Styling

API

Aria Layer

5

Anchor

21 Props3 Emits
Props
PropTypeDefaultDescription
color
ThemeColor
—
Theme color of the component.
size
ThemeSize
—
Visual size of the component.
ui
Partial<AnchorExtendedUi>
—
Per-slot class overrides for the component.
class
ClassValue
—
Additional class names applied to the root element.
sticky
boolean
—
Whether sticky.
items
AnchorOptionData[]
—
Items rendered by the component.
linkProps
Omit<AnchorLinkProps, 'href'>
—
Properties forwarded to the link element.
indicatorProps
HTMLAttributes
—
Properties forwarded to the indicator element.
titleProps
HTMLAttributes
—
Properties forwarded to the title element.
subProps
HTMLAttributes
—
Properties forwarded to the sub element.
bounds
number
—
Bounds.
dir
Direction
—
Reading direction of the component.
getContainer
(() => AnchorContainer | null)
—
Get container.
getCurrentAnchor
((activeHref: string) => string)
—
Get current anchor.
modelValue
string
—
Current model value.
offsetTop
number
—
Offset top.
orientation
DataOrientation
—
Orientation of the component.
replace
boolean
—
Whether replace.
targetOffset
number
—
Target offset.
asChild
boolean
—
Change the default rendered element for the one passed as a child, merging their props and behavior.
as
AsTag | Component
'div'
The element or component this component should render as. Can be overwrite by `asChild`
Emits
Emit NameParametersDescription
update:modelValue
string
Emitted when the model value changes.
activeChange
string
Emitted when active change occurs.
itemSelect
MouseEvent, payload: { href: string; }
Emitted when item select occurs.

AnchorCompact

16 Props3 Emits
Props
PropTypeDefaultDescription
items
AnchorOptionData[]
—
Items rendered by the component.
linkProps
Omit<AnchorLinkProps, 'href'>
—
Properties forwarded to the link element.
indicatorProps
HTMLAttributes
—
Properties forwarded to the indicator element.
titleProps
HTMLAttributes
—
Properties forwarded to the title element.
subProps
HTMLAttributes
—
Properties forwarded to the sub element.
bounds
number
—
Bounds.
dir
Direction
—
Reading direction of the component.
getContainer
(() => AnchorContainer | null)
—
Get container.
getCurrentAnchor
((activeHref: string) => string)
—
Get current anchor.
modelValue
string
—
Current model value.
offsetTop
number
—
Offset top.
orientation
DataOrientation
—
Orientation of the component.
replace
boolean
—
Whether replace.
targetOffset
number
—
Target offset.
asChild
boolean
—
Change the default rendered element for the one passed as a child, merging their props and behavior.
as
AsTag | Component
'div'
The element or component this component should render as. Can be overwrite by `asChild`
Emits
Emit NameParametersDescription
update:modelValue
string
Emitted when the model value changes.
activeChange
string
Emitted when active change occurs.
itemSelect
MouseEvent, payload: { href: string; }
Emitted when item select occurs.

AnchorItemCompact

6 Props
Props
PropTypeDefaultDescription
modelValue
string
—
Current model value.
item
AnchorOptionData
—
Current item data.
linkProps
Omit<AnchorLinkProps, 'href'>
—
Properties forwarded to the link element.
indicatorProps
HTMLAttributes
—
Properties forwarded to the indicator element.
titleProps
HTMLAttributes
—
Properties forwarded to the title element.
subProps
HTMLAttributes
—
Properties forwarded to the sub element.

AnchorLink

7 Props
Props
PropTypeDefaultDescription
href
string
—
The link of anchor
disabled
boolean
—
Whether the component is disabled.
to
string | import("vue-router").RouteLocationAsRelativeGeneric | import("vue-router").RouteLocationAsPathGeneric
—
Route Location the link should navigate to when clicked on.
external
boolean
—
Forces the link to be considered as external (true) or internal (false). This is helpful to handle edge-cases
target
(string & {}) | '_blank' | '_parent' | '_self' | '_top' | null
—
Where to display the linked URL, as the name for a browsing context.
asChild
boolean
—
Change the default rendered element for the one passed as a child, merging their props and behavior.
as
AsTag | Component
'div'
The element or component this component should render as. Can be overwrite by `asChild`

AnchorRoot

11 Props3 Emits
Props
PropTypeDefaultDescription
bounds
number
—
Bounds.
dir
Direction
—
Reading direction of the component.
getContainer
(() => AnchorContainer | null)
—
Get container.
getCurrentAnchor
((activeHref: string) => string)
—
Get current anchor.
modelValue
string
—
Current model value.
offsetTop
number
—
Offset top.
orientation
DataOrientation
—
Orientation of the component.
replace
boolean
—
Whether replace.
targetOffset
number
—
Target offset.
asChild
boolean
—
Change the default rendered element for the one passed as a child, merging their props and behavior.
as
AsTag | Component
'div'
The element or component this component should render as. Can be overwrite by `asChild`
Emits
Emit NameParametersDescription
update:modelValue
string
Emitted when the model value changes.
activeChange
string
Emitted when active change occurs.
itemSelect
MouseEvent, payload: { href: string; }
Emitted when item select occurs.

Notes

Architecture and benchmark comparison

ConcernVeanUIAnt Design AnchorElement Plus Anchor
Aria / styled separation✅ @vean/aria/anchor + scv()❌ single package❌ single package
Data-driven compact API✅ generic AnchorCompact + recursive items✅ items✅ items
Scroll container✅ getContainer (HTMLElement or window)✅ getContainer✅ container
Scroll offset✅ offsetTop + targetOffset✅ offsetTop + targetOffset✅ offset
Active bounds tolerance✅ bounds✅ bounds✅ bound
Active href transform✅ getCurrentAnchor✅ getCurrentAnchor—
Hash sync✅ pushState on click / replaceState on scroll✅✅
Initial hash scroll✅✅—
Sticky rail✅ CSS sticky + offset var✅ affix wrapper✅ CSS sticky
Nested items✅ recursive (arbitrary depth)❌ flat✅ 2 levels
Disabled items✅ per-item + linkProps fallback❌❌
Custom link props✅ linkProps / indicatorProps / titleProps / subProps——
Orientation✅ vertical / horizontal❌ vertical only✅ direction
Theme colors / sizes✅ 8 colors × 6 sizes❌❌
Events✅ activeChange / itemSelect✅ onChange / onClick✅ change

Runtime considerations

  1. Measurement timing — anchor positions are re-measured on every scroll event against the current scroll container. Sections that do not exist yet (or links not yet registered) are skipped, so the active item is only derived from registered links.
  2. Controlled vs. uncontrolled — when modelValue is provided, internal writes only emit update:modelValue; the highlighted link follows the prop and no scrolling is triggered by external value changes.
  3. getContainer instability — the component re-syncs whenever the container reference changes; returning a new element from every call causes repeated re-syncs, so memoize it when the element is created asynchronously.
  4. Hash-only links — only links starting with # participate in scrolling and hash updates; plain/external hrefs are left to the browser’s default navigation (and the itemSelect event still fires).
  5. Scroll-animation guard — during the 300 ms after a programmatic scroll the scroll-spy is suspended so the intermediate positions do not flip the active item or rewrite the hash.
  6. Sticky rail — sticky requires a scrollable ancestor for the rail to actually pin; the offset var keeps the rail below sticky headers. Set sticky: false for non-pinned usage.
  7. Disabled links — a disabled item is inert: aria-disabled="true", tabindex="-1", and click/keyboard activation are blocked (no itemSelect). It stays registered so nested children can still be highlighted.

FAQ

How do I scroll an inner container instead of the window?

Pass getContainer={() => myScrollableEl}. Anchor positions are then measured relative to that element and scrolling is applied to it. When the element becomes available after mount, Anchor re-syncs automatically.

How do I keep headings below a sticky header?

Set offsetTop to the header height. SAnchor scrolls to elementTop - offsetTop and exposes the same value as --vean-anchor-offset-top so the sticky anchor rail also clears the header. Use targetOffset if the anchor offset must differ from the scroll offset.

Why does the address bar update on scroll?

This is intentional — scroll-driven changes use history.replaceState so the URL hash always matches the section in view without flooding the history. Click-driven jumps use pushState unless you set replace.

How is the active section chosen?

A section becomes active once its measured top is at or above offsetTop + bounds. Among the passed sections the one closest to the threshold (the most recently crossed) is highlighted. bounds increases the tolerance; getCurrentAnchor lets you remap the resolved value before it is emitted.

Can I disable or customize individual links?

Yes — each item supports disabled and target; global defaults go through linkProps. Because the item’s href always owns the destination, linkProps.href is not part of the type. indicatorProps / titleProps / subProps forward HTML attributes to the per-item elements.

What does the nested composition look like?

Each item renders an AnchorLink (indicator + title) and, when it has children, a sub container that recursively renders AnchorItemCompact levels. The wrapper carries data-vean-anchor-item with a data-state reflecting whether the item or any of its descendants is active.

Changelog

Version history is grouped by release so you can quickly see what changed for this component across published versions.

v0.50.0

2026-10-02

Compare release

Style

update focus-visible ring styles for link variants

Scope: anchorAuthors: soybeanjsView commit56bdb

Style

update focus-visible ring styles for link variants

Scope: anchorAuthors: soybeanjsView commit56bdb

v0.40.0

2026-09-14

Compare release

Fix

preserve history state in hash sync

Scope: anchorAuthors: soybeanfeView commita2a1e

Fix

preserve history state in hash sync

Scope: anchorAuthors: soybeanfeView commita2a1e

v0.17.0

2026-05-03

Compare release

Refactor

rename AnchorItemCompact component and update types

Scope: anchorAuthors: soybeanjsView commit11966

v0.15.0

2026-04-26

Compare release

Optimization

enhance anchor component with location hash sync and optimize code

Scope: anchorAuthors: soybeanjsView commit3b126

Optimization

enhance anchor component with location hash sync and optimize code

Scope: anchorAuthors: soybeanjsView commit3b126