Loading...
Please waitLoading...
Please waitTrack which scroll section is active, just like docs sites that sync the sidebar with the content scroller.
pnpm dlx uselab@latest add use-section-tracker
import * as React from "react"
import {
useSectionTracker,
type TrackedSectionConfig,
} from "@/hooks/use-section-tracker"
export function DocsLayout({ children }: { children: React.ReactNode }) {
const sections = ["introduction", "setup", "usage"].map((id) => ({
id,
ref: React.useRef<HTMLElement | null>(null),
}))
const { activeSection } = useSectionTracker(
sections as TrackedSectionConfig[]
)
return (
<div className="grid gap-4 md:grid-cols-[200px,minmax(0,1fr)]">
<nav className="space-y-1 text-sm">
{sections.map((section) => (
<button
key={section.id}
type="button"
className={
activeSection === section.id
? "font-semibold"
: "text-muted-foreground"
}
>
{section.id}
</button>
))}
</nav>
<div>{children}</div>
</div>
)
}useSectionTracker(sections, options?)Given a list of section refs, returns the id of the section that is currently considered “active” in the viewport.
| Name | Type | Description | Optional | Default |
|---|---|---|---|---|
sections | TrackedSectionConfig[] | Array of section descriptors ({ id, ref }). Each ref should point to a scrollable block. | No | — |
options | UseSectionTrackerOptions | Configuration object for intersection behavior. | Yes | {} |
rootMargin | string | Margin around the viewport used for IntersectionObserver (e.g. "0px 0px -40% 0px"). | Yes | "0px 0px -40% 0px" |
threshold | number | Minimum intersection ratio (0–1) before a section is considered for activation. | Yes | 0 |
The hook returns an object { activeSection }:
| Name | Type | Description |
|---|---|---|
activeSection | string | null | The id of the currently active section, or null when none qualify. |
export interface TrackedSectionConfig {
id: string
ref: React.RefObject<HTMLElement | null>
}
export interface UseSectionTrackerOptions {
rootMargin?: string
threshold?: number
}
export interface UseSectionTrackerResult {
activeSection: string | null
}IntersectionObserver to watch each section element and record:
intersectionRatioboundingClientRect.top relative to the viewport.threshold and whose top is within the viewport.activeSection only when the winning id changes.element.scrollIntoView({ behavior: "smooth" })) in your nav buttons to create a polished experience.