Components
- Accordion new
- ActionSheet new
- Alert new
- Alert Dialog new
- Angle Slider new
- Autocomplete new
- Avatar new
- Badge new
- Breadcrumb new
- Button new
- Button Group new
- Card new
- Carousel new
- Checkbox new
- Clipboard new
- Collapsible new
- Color Picker new
- Date Input new
- Date Picker new
- Dialog new
- DialogCaller new
- Drawer beta
- Editable new
- Empty new
- Field new
- Fieldset new
- File Upload new
- Filters new
- Floating Panel new
- Hover Card new
- Image new
- Image Cropper new
- Input new
- Input Group new
- Json Tree View new
- Kbd new
- Listbox new
- Marquee new
- Menu new
- Number Input new
- Pagination new
- Password Input new
- PhoneField new
- Pin Input new
- Popover new
- Progress Circular new
- Progress Linear new
- Qr Code new
- Radio Group new
- Rating Group new
- Scroll Area new
- ScrollSpy new
- Segment Group new
- Select new
- Separator new
- Sidebar new
- Signature Pad new
- Skeleton new
- Slider new
- Splitter new
- Steps new
- Switch new
- Table new
- Tabs new
- Tags Input new
- Textarea new
- Timeline new
- Timer new
- Toast new
- Toc new
- Toggle new
- Toggle Group new
- Tooltip new
- Tour new
- Tree View new
- Typography new
Features
- Active tracking — Scroll position updates
v-modelfrom a configurable top offset threshold - Viewport —
ScrollSpy.Viewportis the scroll container that drives tracking - Zag machine — Behavior is owned by a Zag.js state machine (
useScrollSpy, Provider) - Composable — Provide active state to Toc (or custom UI) through context
Installation
Install from the Vuzeno registry:
bunx --bun shadcn-vue@latest add https://vuzeno.com/r/scroll-spy.json
npx shadcn-vue@latest add https://vuzeno.com/r/scroll-spy.json
yarn dlx shadcn-vue@latest add https://vuzeno.com/r/scroll-spy.json
pnpm dlx shadcn-vue@latest add https://vuzeno.com/r/scroll-spy.json
Usage
<script setup lang="ts">
import { ScrollSpy } from "@/components/ui/scroll-spy";
import { ref } from "vue";
const active = ref("intro");
</script>
<template>
<ScrollSpy.Root v-model="active">
<ScrollSpy.Viewport class="h-96 overflow-y-auto">
<ScrollSpy.Item value="intro">
<h2 id="intro">Intro</h2>
<p>...</p>
</ScrollSpy.Item>
<ScrollSpy.Item value="usage">
<h2 id="usage">Usage</h2>
<p>...</p>
</ScrollSpy.Item>
</ScrollSpy.Viewport>
</ScrollSpy.Root>
</template>
Root provider
Drive the machine yourself with useScrollSpy and mount parts under ScrollSpy.Provider:
<script setup lang="ts">
import { ScrollSpy, useScrollSpy } from "@/components/ui/scroll-spy";
const api = useScrollSpy({ defaultValue: "intro" });
</script>
<template>
<ScrollSpy.Provider :value="api">
<ScrollSpy.Viewport class="h-96 overflow-y-auto">
<ScrollSpy.Item value="intro">Intro</ScrollSpy.Item>
</ScrollSpy.Viewport>
</ScrollSpy.Provider>
</template>
Composition
ScrollSpy.Root / ScrollSpy.Provider
├── ScrollSpy.Viewport
│ └── ScrollSpy.Item
└── ScrollSpy.Target
API Reference
ScrollSpy.Root
| Prop | Type | Default |
|---|---|---|
v-model | string | "" |
offset | number | 0.25 |
orientation | "vertical" | "horizontal" | "vertical" |
offset accepts a ratio (0–1) of the scroll root size, or an absolute pixel value.
ScrollSpy.Item
| Prop | Type | Default |
|---|---|---|
value | string | — |
ScrollSpy.Target
| Prop | Type | Default |
|---|---|---|
value | string | — |
Registers an existing document element by id (document.getElementById(value)). Use this when sections are rendered outside ScrollSpy.Viewport — for example a page sidebar TOC watching article headings. Without a viewport, ScrollSpy.Root tracks against the window.
Notes
- Pair with Toc for a sidebar rail that follows the active section.
- Prefer
ScrollSpy.Viewportas the scroll container so tracking binds reliably. - Use
ScrollSpy.Targetwhen you cannot wrap the scrolled content inScrollSpy.Item. - Export
useScrollSpy,machine,connect, andanatomyfor headless composition.