Anchor
anchorTracks and navigates headings within long-form page content.
Usage
Linkage of table of contents and long articles
sticky directory on the left + independent scrolling container on the right. When scrolling the text, the highlight and indicator bar follow the current chapter, and click the table of contents to jump smoothly.
Overview
Anchor navigation follows the reading progress to highlight the current chapter and smoothly scrolls to the target position when clicked. It is suitable for any reading page with "catalog on the left + long text on the right": API document, privacy agreement, product description, section form of settings page.
Its core is a zero-dependency scrollspy: Internally, IntersectionObserver is used to observe each section, and the "frontmost visible item in the document sequence" is taken as the current anchor point; the sliding indicator bar on the left reuses the same "Write active geometry into" Tabs "CSS variable, pure CSS transition smooth transition" technique, does not rely on any animation library when running.
Scroll down this text, and you will see the highlight and indicator bar of the table of contents on the left move with the chapter you are currently reading; click on any item in the table of contents, and the right side will scroll smoothly to the corresponding section.
Get started quickly
Divided into three steps: installation, adding id to each chapter in the content area, and feeding the same structure to items of Anchor.
Installation
After installing the component library through the package manager, you can import it on demand: import { Anchor } from "@hulianui/ui". The component comes with the "use client" mark and can be used directly as a client island in the React Server Component page.
No need to introduce additional style files or animation runtimes - indicator bars and highlights are all semantic token, automatically adapting to light and dark themes.
Basic usage
Give each chapter element a unique id, and then pass the corresponding { href, title } list to items. href is in the shape of "#section-id", which corresponds one-to-one to id of the element on the page.
When there are many chapters and long content, it is recommended to fix the table of contents sticky to the side of the viewport so that the table of contents is always visible when scrolling the text - this is what is done on the left side of this example.
Secondary anchor point
Provide children on a certain item to form a secondary directory, which will be automatically indented during rendering. scrollspy will flatten the parent and child items together to participate in the calculation, so scroll to any subsection, the corresponding second-level item will be highlighted, and the indicator bar will slide to it.
There is a deliberate restriction within the second level: directories with more than two levels will quickly lose readability in narrow side columns. It is better to use a collapsible tree navigation instead.
API
Three core attributes cover most scenarios.
items
AnchorItem[] —— Required. Each item contains href, title, and children is optional to form the second level. title accepts ReactNode so you can plug an icon or logo.
offsetTop
number, default 0. Set it when the page has a fixed header: it will not only reserve this distance for the top of the target to avoid occlusion when clicking to scroll, but also shrink the observation upper edge of scrollspy synchronously to align the highlight judgment with vision.
getContainer
() => HTMLElement | null, optional. By default, the viewport/window is the scroll root. When the real scrolling body of the page is not window (such as the container of inner overflow-y-auto), the observation root and click scrolling of scrollspy, the function that returns the container, will fall on it.
The right side of this example is an independent scrolling container: Anchor points to it through getContainer, so highlight follow and click jump are established inside this box without scrolling the entire page.
FAQ
Can't click on the directory? Most likely, the real scrolling body of the page is not window - just use getContainer to point to the overflow container.
The highlight is always half a beat slower than the visual or misaligned? Check whether offsetTop is equal to the height of the fixed header.
Want to use the browser's native hash scrolling? Connect onChange to the route and let the anchor point change be written back to URL, so that you can obtain a shareable deep link without destroying scrollspy.
const ref = useRef<HTMLDivElement>(null);
<div className="flex gap-6">
<Anchor items={items} getContainer={() => ref.current} className="sticky top-0 w-40" />
<div ref={ref} className="h-80 overflow-y-auto">
{/* Each chapter has id corresponding to href */}
</div>
</div>Secondary anchor point
Providing children on an item will form a second-level directory, which will be automatically indented during rendering, and scrollspy will be flattened to participate in the calculation.
<Anchor
items={[
{ href: "#sec-overview", title: "Overview" },
{
href: "#sec-guide",
title: "Get started quickly",
children: [
{ href: "#sec-install", title: "Installation" },
{ href: "#sec-usage", title: "Basic usage" },
],
},
]}
className="w-48"
/>Avoid fixed header
When the page has a fixed header, set offsetTop=header height, click to scroll to reserve this distance, and highlight to determine synchronization alignment.
Overview
Anchor navigation follows the reading progress to highlight the current chapter and smoothly scrolls to the target position when clicked. It is suitable for any reading page with "catalog on the left + long text on the right": API document, privacy agreement, product description, section form of settings page.
Its core is a zero-dependency scrollspy: Internally, IntersectionObserver is used to observe each section, and the "frontmost visible item in the document sequence" is taken as the current anchor point; the sliding indicator bar on the left reuses the same "Write active geometry into" Tabs "CSS variable, pure CSS transition smooth transition" technique, does not rely on any animation library when running.
Scroll down this text, and you will see the highlight and indicator bar of the table of contents on the left move with the chapter you are currently reading; click on any item in the table of contents, and the right side will scroll smoothly to the corresponding section.
Get started quickly
Divided into three steps: installation, adding id to each chapter in the content area, and feeding the same structure to items of Anchor.
Installation
After installing the component library through the package manager, you can import it on demand: import { Anchor } from "@hulianui/ui". The component comes with the "use client" mark and can be used directly as a client island in the React Server Component page.
No need to introduce additional style files or animation runtimes - indicator bars and highlights are all semantic token, automatically adapting to light and dark themes.
Basic usage
Give each chapter element a unique id, and then pass the corresponding { href, title } list to items. href is in the shape of "#section-id", which corresponds one-to-one to id of the element on the page.
When there are many chapters and long content, it is recommended to fix the table of contents sticky to the side of the viewport so that the table of contents is always visible when scrolling the text - this is what is done on the left side of this example.
Secondary anchor point
Provide children on a certain item to form a secondary directory, which will be automatically indented during rendering. scrollspy will flatten the parent and child items together to participate in the calculation, so scroll to any subsection, the corresponding second-level item will be highlighted, and the indicator bar will slide to it.
There is a deliberate restriction within the second level: directories with more than two levels will quickly lose readability in narrow side columns. It is better to use a collapsible tree navigation instead.
API
Three core attributes cover most scenarios.
items
AnchorItem[] —— Required. Each item contains href, title, and children is optional to form the second level. title accepts ReactNode so you can plug an icon or logo.
offsetTop
number, default 0. Set it when the page has a fixed header: it will not only reserve this distance for the top of the target to avoid occlusion when clicking to scroll, but also shrink the observation upper edge of scrollspy synchronously to align the highlight judgment with vision.
getContainer
() => HTMLElement | null, optional. By default, the viewport/window is the scroll root. When the real scrolling body of the page is not window (such as the container of inner overflow-y-auto), the observation root and click scrolling of scrollspy, the function that returns the container, will fall on it.
The right side of this example is an independent scrolling container: Anchor points to it through getContainer, so highlight follow and click jump are established inside this box without scrolling the entire page.
FAQ
Can't click on the directory? Most likely, the real scrolling body of the page is not window - just use getContainer to point to the overflow container.
The highlight is always half a beat slower than the visual or misaligned? Check whether offsetTop is equal to the height of the fixed header.
Want to use the browser's native hash scrolling? Connect onChange to the route and let the anchor point change be written back to URL, so that you can obtain a shareable deep link without destroying scrollspy.
const ref = useRef<HTMLDivElement>(null);
<Anchor items={items} offsetTop={64} getContainer={() => ref.current} />When to use
Use Anchor for a table of contents beside long-form material such as API documentation, privacy policies, product guides, or sectioned settings. It highlights the current section while scrolling and smoothly moves to a section when selected. Use Tabs for mutually exclusive peer content, Breadcrumb for hierarchy, or wrap Anchor in Affix to keep the table of contents visible while scrolling.
Import
import { Anchor, flattenAnchorItems } from "@hulianui/ui"Props
| Name | Type | Default | Description |
|---|---|---|---|
| items* | AnchorItem[] | — | Anchor entries with one optional child level. |
| offsetTop | number | 0 | Space reserved above the target in pixels, typically for a fixed header. Also moves the upper scrollspy boundary. |
| getContainer | () => HTMLElement | null | undefined (window) | Custom scroll container. Required when the actual scroller is not window. |
AnchorItem is { href: string; title: ReactNode; children?: AnchorItem[] }. Each href, such as "#section-id", must match an element id on the page.
The inherited aria-label defaults to the built-in Chinese copy "\u951a\u70b9\u5bfc\u822a", meaning “Anchor navigation.” Pass an English label for an English interface.
Events
| Event | Type | Description |
|---|---|---|
| onChange | (href: string) => void | Called when the active anchor changes through a click or scrolling; repeated identical values are suppressed. |
Example
<Anchor
items={[
{ href: "#sec-overview", title: "Overview" },
{
href: "#sec-guide",
title: "Quick start",
children: [
{ href: "#sec-install", title: "Installation" },
{ href: "#sec-usage", title: "Basic usage" },
],
},
{ href: "#sec-faq", title: "FAQ" },
]}
/>For an inner overflow-y-auto scroller:
<Anchor items={items} getContainer={() => document.querySelector("main")} />Usage guidelines
- [[scrollspy-anchor-hardcoded-window-scroll-breaks-in-inner-container]]: when an inner element such as
<main class="overflow-y-auto">orLayout.Contentis the true scroller, pass it throughgetContainer. Otherwise both the IntersectionObserver root and click scrolling target the wrong container, so scrolling and highlighting fail. - One nested level is intentional. Deeper tables of contents quickly become unreadable in a narrow rail; use collapsible tree navigation instead.
- The component includes
"use client"and can be used as a client island within an RSC page.
Related
Tabs · Breadcrumb · Pagination · Affix · BackTop · Stepper
Playground
Overview
Anchor navigation follows the reading progress to highlight the current chapter and smoothly scrolls to the target position when clicked. It is suitable for any reading page with "catalog on the left + long text on the right": API document, privacy agreement, product description, section form of settings page.
Its core is a zero-dependency scrollspy: Internally, IntersectionObserver is used to observe each section, and the "frontmost visible item in the document sequence" is taken as the current anchor point; the sliding indicator bar on the left reuses the same "Write active geometry into" Tabs "CSS variable, pure CSS transition smooth transition" technique, does not rely on any animation library when running.
Scroll down this text, and you will see the highlight and indicator bar of the table of contents on the left move with the chapter you are currently reading; click on any item in the table of contents, and the right side will scroll smoothly to the corresponding section.
Get started quickly
Divided into three steps: installation, adding id to each chapter in the content area, and feeding the same structure to items of Anchor.
Installation
After installing the component library through the package manager, you can import it on demand: import { Anchor } from "@hulianui/ui". The component comes with the "use client" mark and can be used directly as a client island in the React Server Component page.
No need to introduce additional style files or animation runtimes - indicator bars and highlights are all semantic token, automatically adapting to light and dark themes.
Basic usage
Give each chapter element a unique id, and then pass the corresponding { href, title } list to items. href is in the shape of "#section-id", which corresponds one-to-one to id of the element on the page.
When there are many chapters and long content, it is recommended to fix the table of contents sticky to the side of the viewport so that the table of contents is always visible when scrolling the text - this is what is done on the left side of this example.
Secondary anchor point
Provide children on a certain item to form a secondary directory, which will be automatically indented during rendering. scrollspy will flatten the parent and child items together to participate in the calculation, so scroll to any subsection, the corresponding second-level item will be highlighted, and the indicator bar will slide to it.
There is a deliberate restriction within the second level: directories with more than two levels will quickly lose readability in narrow side columns. It is better to use a collapsible tree navigation instead.
API
Three core attributes cover most scenarios.
items
AnchorItem[] —— Required. Each item contains href, title, and children is optional to form the second level. title accepts ReactNode so you can plug an icon or logo.
offsetTop
number, default 0. Set it when the page has a fixed header: it will not only reserve this distance for the top of the target to avoid occlusion when clicking to scroll, but also shrink the observation upper edge of scrollspy synchronously to align the highlight judgment with vision.
getContainer
() => HTMLElement | null, optional. By default, the viewport/window is the scroll root. When the real scrolling body of the page is not window (such as the container of inner overflow-y-auto), the observation root and click scrolling of scrollspy, the function that returns the container, will fall on it.
The right side of this example is an independent scrolling container: Anchor points to it through getContainer, so highlight follow and click jump are established inside this box without scrolling the entire page.
FAQ
Can't click on the directory? Most likely, the real scrolling body of the page is not window - just use getContainer to point to the overflow container.
The highlight is always half a beat slower than the visual or misaligned? Check whether offsetTop is equal to the height of the fixed header.
Want to use the browser's native hash scrolling? Connect onChange to the route and let the anchor point change be written back to URL, so that you can obtain a shareable deep link without destroying scrollspy.
// Inner scrolling container scene: getContainer points to the real scrolling body
const ref = useRef<HTMLDivElement>(null);
<div className="flex gap-6">
<Anchor items={items} offsetTop={8} getContainer={() => ref.current} className="sticky top-0" />
<div ref={ref} className="h-80 overflow-y-auto">{/* Chapters with id */}</div>
</div>