Animated Timeline
A vertical timeline with a glowing beam that fills as you scroll.
Installation
Add this component to your project using the CLI:
terminal
npx vui-registry-cli-v1 add timelineSource Code
timeline.tsx
'use client'
import React, { useRef, useState, useEffect } from 'react'
import { motion, useScroll, useSpring, useTransform } from 'framer-motion'
import { cn } from '@/lib/utils'
interface TimelineProps {
items: {
title: string
date: string
description: string
icon?: React.ReactNode
}[]
className?: string
}
export function Timeline({ items, className }: TimelineProps) {
const containerRef = useRef<HTMLDivElement>(null)
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ['start end', 'end start'],
})
const scaleY = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001
})
return (
<div ref={containerRef} className={cn('relative w-full max-w-2xl mx-auto py-20', className)}>
{/* Central Line */}
<div className="absolute left-[20px] md:left-1/2 top-0 bottom-0 w-[2px] bg-border md:-translate-x-1/2" />
{/* Animated Filling Line */}
<motion.div
style={{ scaleY }}
className="absolute left-[20px] md:left-1/2 top-0 bottom-0 w-[2px] bg-gradient-to-b from-primary via-primary/50 to-transparent origin-top md:-translate-x-1/2 z-10"
/>
<div className="space-y-12">
{items.map((item, index) => (
<TimelineItem key={index} item={item} index={index} />
))}
</div>
</div>
)
}
function TimelineItem({ item, index }: { item: any; index: number }) {
const isEven = index % 2 === 0
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.5, delay: index * 0.1 }}
className={cn(
'relative flex items-center md:justify-between',
isEven ? 'md:flex-row-reverse' : ''
)}
>
{/* Content */}
<div className="w-[calc(100%-60px)] md:w-[45%] pl-12 md:pl-0">
<div className="p-6 rounded-2xl border border-border/50 bg-background/50 backdrop-blur-sm hover:border-primary/50 transition-colors shadow-sm">
<div className="flex items-center justify-between mb-2">
<h3 className="font-semibold text-lg">{item.title}</h3>
<span className="text-xs font-mono text-muted-foreground px-2 py-1 rounded bg-muted/50">
{item.date}
</span>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">
{item.description}
</p>
</div>
</div>
{/* Node */}
<div className="absolute left-[20px] md:left-1/2 w-4 h-4 rounded-full bg-background border-2 border-primary z-20 md:-translate-x-1/2 shadow-[0_0_10px_rgba(var(--primary-rgb),0.5)]">
<div className="absolute inset-0 rounded-full bg-primary/20 animate-ping" />
</div>
{/* Empty Space for layout balance */}
<div className="hidden md:block w-[45%]" />
</motion.div>
)
}
export function TimelinePreview() {
const items = [
{
title: 'Project Inception',
date: 'Jan 2024',
description: 'Initial brainstorming and feasibility study for the new platform architecture.',
},
{
title: 'Design System',
date: 'Mar 2024',
description: 'Established the core design tokens, typography, and component library.',
},
{
title: 'Beta Launch',
date: 'Jun 2024',
description: 'Released the MVP to a closed group of beta testers for feedback.',
},
{
title: 'Global Release',
date: 'Aug 2024',
description: 'Official public launch with full feature set and documentation.',
},
]
return (
<div className="w-full">
<Timeline items={items} />
</div>
)
}
Dependencies
framer-motion: latestclsx: latesttailwind-merge: latest
Props
Component property reference.
| Name | Type | Default | Description |
|---|---|---|---|
| items | Array<{ title: string; date: string; description: string; icon?: React.ReactNode }> | [] | Timeline entries to render. |
| className | string | undefined | Additional CSS classes. |
Context Worth Keeping In Orbit
Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.
I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.
I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.

