Table of Contents
nexvyn/table-of-contentsFreeComponent
A fixed bottom-left floating block that tracks scroll position, shows the active section, and expands into a navigable section list.
Live preview
live · rendered by the registry
Rendered by nexvyn on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://ui.nexvyn.dev/r/table-of-contents.jsonSource
1'use client'23import { useState, useEffect, useRef, useCallback, type RefObject } from 'react'4import { motion, AnimatePresence, useReducedMotion } from 'motion/react'5import { cn } from '@/lib/utils'6import { playHoverSound, playClickSound } from '@/lib/sound'7import { CircularProgress } from './circular-progress'89export interface TOCSection {10 id: string11 title: string12 level?: number13}1415interface TableOfContentsProps {16 sections: TOCSection[]17 scrollOffset?: number18 showAfterScroll?: number19 scrollContainer?: RefObject<HTMLElement | null>20 className?: string21}2223export function TableOfContents({24 sections,25 scrollOffset = 120,26 showAfterScroll = 300,27 scrollContainer,28 className,29}: TableOfContentsProps) {30 const [isExpanded, setIsExpanded] = useState(false)31 const [isVisible, setIsVisible] = useState(false)32 const [activeId, setActiveId] = useState<string>('')33 const [scrollProgress, setScrollProgress] = useState(0)34 const containerRef = useRef<HTMLDivElement>(null)35 const rafRef = useRef<number>(0)36 const prefersReduced = useReducedMotion()3738 useEffect(() => {39 let ticking = false40 const el = scrollContainer?.current4142 const getScrollTop = () => (el ? el.scrollTop : window.scrollY)43 const getScrollHeight = () =>44 el45 ? el.scrollHeight - el.clientHeight46 : document.documentElement.scrollHeight - window.innerHeight4748 const handleScroll = () => {49 if (!ticking) {50 rafRef.current = requestAnimationFrame(() => {51 const scrollTop = getScrollTop()52 const height = getScrollHeight()53 const scrolled = height > 0 ? (scrollTop / height) * 100 : 05455 setScrollProgress(scrolled)56 setIsVisible(scrollTop > showAfterScroll)57 ticking = false58 })59 ticking = true60 }61 }6263 const target = el || window64 target.addEventListener('scroll', handleScroll, { passive: true })65 handleScroll()6667 return () => {68 target.removeEventListener('scroll', handleScroll)69 cancelAnimationFrame(rafRef.current)70 }71 }, [showAfterScroll, scrollContainer])7273 useEffect(() => {74 const root = scrollContainer?.current ?? null75 const observer = new IntersectionObserver(76 (entries) => {77 const visible = entries78 .filter((e) => e.isIntersecting)79 .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top)8081 if (visible.length > 0) {82 setActiveId(visible[0].target.id)83 }84// … truncated
What it pulls in
npm packages
Files it writes
More from nexvyn
All 33 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Badgebadge | nexvyn | Components | Free | 2 deps · 2 files | |
| Bars Themebars-theme | nexvyn | Components | Free | 1 dep · 3 files | |
| Bounce Sidebarbounce-sidebar | nexvyn | Components | Free | 1 dep · 2 files | |
| Breadcrumbsbreadcrumbs | nexvyn | Components | Free | 1 dep · 2 files | |
| Checkboxcheckbox | nexvyn | Components | Free | 1 dep · 3 files | |
| Clipboard Fieldclipboard-field | nexvyn | Components | Free | no deps · 2 files | |
| Color Pickercolor-picker | nexvyn | Components | Free | no deps · 16 files | |
| Comboboxcombobox | nexvyn | Components | Free | 1 dep · 2 files |
