Thread Timeline
loomui/thread-timelineFreeComponent
A timeline whose thread is sewn down the page as you read, lighting each node as it reaches it.
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/thread-timeline.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface ThreadTimelineProps extends React.ComponentProps<"ol"> {8 /** Where the thread head sits in the viewport, 0 at the top, 1 at the bottom. */9 line?: number10}1112export function ThreadTimeline({13 children,14 className,15 line = 0.55,16 style,17 ...props18}: ThreadTimelineProps) {19 const ref = React.useRef<HTMLOListElement>(null)2021 React.useEffect(() => {22 const list = ref.current23 if (!list) {24 return25 }2627 const settle = (progress: number, reached: boolean) => {28 list.style.setProperty("--thread-progress", `${progress}`)29 for (const item of list.querySelectorAll<HTMLElement>(30 "[data-slot='thread-timeline-item']"31 )) {32 item.toggleAttribute("data-reached", reached)33 }34 }3536 if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {37 settle(1, true)38 return39 }4041 let frame = 04243 const measure = () => {44 const head = window.innerHeight * line45 const rect = list.getBoundingClientRect()46 const progress = rect.height > 0 ? (head - rect.top) / rect.height : 147 list.style.setProperty(48 "--thread-progress",49 `${Math.min(Math.max(progress, 0), 1)}`50 )5152 // Each node is measured against the same head the thread is drawn to, so53 // a node can never light up ahead of the thread that reaches it.54 for (const item of list.querySelectorAll<HTMLElement>(55 "[data-slot='thread-timeline-item']"56 )) {57 const node = item.querySelector<HTMLElement>(58 "[data-slot='thread-timeline-node']"59 )60 const box = (node ?? item).getBoundingClientRect()61 item.toggleAttribute("data-reached", box.top + box.height / 2 <= head)62 }63 }6465 const onScroll = () => {66 cancelAnimationFrame(frame)67 frame = requestAnimationFrame(measure)68 }6970 measure()71 window.addEventListener("scroll", onScroll, { passive: true })72 window.addEventListener("resize", onScroll, { passive: true })73 return () => {74 window.removeEventListener("scroll", onScroll)75 window.removeEventListener("resize", onScroll)76 cancelAnimationFrame(frame)77 }78 }, [line])7980 return (81 <ol82 ref={ref}83 data-slot="thread-timeline"84 className={cn("relative", className)}85 style={{ "--thread-progress": "0", ...style } as React.CSSProperties}86 {...props}87 >88// … 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 |
