Reading Progress
loomui/reading-progressFreeComponent
A pinned bar that tracks how far through the page, or a chosen element, the reader is.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/reading-progress.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface ReadingProgressProps extends React.ComponentProps<"div"> {8 /** Which edge the bar pins to. */9 position?: "top" | "bottom"10 /** Bar thickness in pixels. */11 thickness?: number12 /** Measure this element instead of the whole document. */13 target?: React.RefObject<HTMLElement | null>14 /** Render nothing. Useful for pages that should not have a bar. */15 disabled?: boolean16}1718export function ReadingProgress({19 className,20 position = "top",21 thickness = 2,22 target,23 disabled = false,24 style,25 ...props26}: ReadingProgressProps) {27 const ref = React.useRef<HTMLDivElement>(null)2829 React.useEffect(() => {30 if (disabled) {31 return32 }3334 let frame = 03536 const measure = () => {37 const node = ref.current38 if (!node) {39 return40 }4142 let progress: number4344 if (target?.current) {45 const rect = target.current.getBoundingClientRect()46 const scrollable = rect.height - window.innerHeight47 progress = scrollable > 0 ? -rect.top / scrollable : 148 } else {49 const scrollable =50 document.documentElement.scrollHeight - window.innerHeight51 progress = scrollable > 0 ? window.scrollY / scrollable : 152 }5354 // Scrolling is the highest-frequency input there is, so the bar tracks55 // it exactly. No transition, no easing, no perceived lag.56 node.style.scale = `${Math.min(Math.max(progress, 0), 1)} 1`57 }5859 const onScroll = () => {60 cancelAnimationFrame(frame)61 frame = requestAnimationFrame(measure)62 }6364 measure()65 window.addEventListener("scroll", onScroll, { passive: true })66 window.addEventListener("resize", onScroll, { passive: true })67 return () => {68 window.removeEventListener("scroll", onScroll)69 window.removeEventListener("resize", onScroll)70 cancelAnimationFrame(frame)71 }72 }, [disabled, target])7374 if (disabled) {75 return null76 }7778 return (79 <div80 data-slot="reading-progress"81 // Decorative. The scrollbar already carries this information, and a82 // `progressbar` role without a live `aria-valuenow` announces worse than83 // nothing at all.84 aria-hidden="true"85 className={cn(86 "pointer-events-none fixed inset-x-0 z-50",87 position === "top" ? "top-0" : "bottom-0",88 className89 )}90 style={{ height: thickness, ...style }}91 {...props}92 >93// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Card Stackcard-stack | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep |
