Custom Shadcn Scrollspy for React and Tailwind CSS. Dynamically highlights navigation to indicate current visible section in viewport during page scroll
Browse 2 production-ready Shadcn Scrollspy components for dashboards, forms, and product UI. These examples use Base UI primitives from @base-ui/react and stay fully compatible with Shadcn Create so radius, color, and typography match your configured theme.
Browse all 2 Shadcn Scrollspy components for copy-ready layouts, dashboards, and forms built with Tailwind CSS in the ReUI library.
import { Scrollspy } from "@/components/reui/scrollspy"<Scrollspy targetRef={containerRef}>
<a href="#section-1" data-scrollspy-anchor="section-1">Section 1</a>
<a href="#section-2" data-scrollspy-anchor="section-2">Section 2</a>
</Scrollspy>
<div ref={containerRef}>
<div id="section-1">Content 1</div>
<div id="section-2">Content 2</div>
</div>The main component that wraps the navigation links and manages the scroll spying logic.
Navigation links within Scrollspy should use these attributes to connect to sections:
The component adds data-active="true" to the link element when its corresponding section is active.
"use client"
import { useRef } from "react"
import { Scrollspy } from "@/components/reui/scrollspy"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
export function Pattern() {
const parentRef = useRef<HTMLDivElement | null>(null)
const nav = [
{
id: "section-1",
label: "Section 1",
},
{
id: "section-2",
label: "Section 2",
},
{
id: "section-3",
label: "Section 3",
},
{
id: "section-4",
label: "Section 4",
},
{
id: "section-5",
label: "Section 5",
},
]
return (
<div className="flex w-full grow gap-5">
<div className="flex w-[150px] flex-col gap-2">
<Scrollspy
offset={50}
targetRef={parentRef}
className="flex flex-col gap-2.5"
>
{nav.map((item) => (
<Button
key={item.id}
variant="outline"
data-scrollspy-anchor={item.id}
className={
"data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
}
>
{item.label}
</Button>
))}
</Scrollspy>
</div>
<div className="grow" ref={parentRef}>
<ScrollArea className="-me-5 h-[500px] grow pe-5">
<div className="space-y-8">
{nav.map((item) => (
<div key={item.id} id={item.id} className="space-y-2.5">
<h3 className="text-foreground text-base">{item.label}</h3>
<div className="bg-muted rounded-2xl h-[350px]"></div>
</div>
))}
</div>
</ScrollArea>
</div>
</div>
)
}
"use client"
import { useRef } from "react"
import { Scrollspy } from "@/components/reui/scrollspy"
import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
export function Pattern() {
const parentRef = useRef<HTMLDivElement>(null)
const nav = [
{
id: "section-6",
label: "Section 1",
},
{
id: "section-7",
label: "Section 2",
},
{
id: "section-8",
label: "Section 3",
},
{
id: "section-9",
label: "Section 4",
},
{
id: "section-10",
label: "Section 5",
},
]
return (
<div className="w-full space-y-5">
<div className="flex w-full gap-2">
<Scrollspy offset={50} targetRef={parentRef} className="flex gap-2.5">
{nav.map((item) => (
<Button
key={item.id}
variant="outline"
data-scrollspy-anchor={item.id}
className={
"data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
}
>
{item.label}
</Button>
))}
</Scrollspy>
</div>
<div className="w-full" ref={parentRef}>
<ScrollArea className="h-[400px] grow">
<div className="space-y-8">
{nav.map((item) => (
<div key={item.id} id={item.id} className="space-y-2.5">
<h3 className="text-foreground text-base">{item.label}</h3>
<div className="bg-muted rounded-2xl h-[350px]"></div>
</div>
))}
</div>
</ScrollArea>
</div>
</div>
)
}