An interactive horizontal timeline with smooth scrolling, animated progress tracking, and responsive design. Perfect for showcasing project milestones, company history, or any chronological events.
From concept to launch. Every milestone that shaped our product.
npm i motion clsx tailwind-merge lucide-react
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
"use client"
import { useRef, useState, useEffect } from "react"
import { motion, useScroll } from "framer-motion"
import { ChevronLeft, ChevronRight, ArrowRight } from "lucide-react"
import { Button } from "@/components/ui/button"
const timelineEvents = [
{
id: 1,
date: "2024.01",
title: "Project Kickoff",
description:
"Initial planning and team formation. Set ambitious goals for the year ahead with a focus on user experience.",
tag: "Launch",
},
{
id: 2,
date: "2024.02",
title: "First Prototype",
description: "Built and tested our first working prototype with core functionality and essential user flows.",
tag: "Development",
},
{
id: 3,
date: "2024.03",
title: "User Research",
description: "Conducted extensive user testing with 50+ participants across different demographics and use cases.",
tag: "Research",
},
{
id: 4,
date: "2024.04",
title: "Beta Launch",
description: "Soft launch to a select group of beta users. Gathered valuable feedback and iterated rapidly.",
tag: "Launch",
},
{
id: 5,
date: "2024.05",
title: "Public Release",
description: "Official public launch with full marketing campaign and comprehensive press coverage.",
tag: "Milestone",
},
{
id: 6,
date: "2024.06",
title: "1K Users",
description: "Reached our first major user milestone with 1,000 active users and positive community feedback.",
tag: "Growth",
},
{
id: 7,
date: "2024.07",
title: "Feature Update",
description: "Major feature update based on user feedback and comprehensive market research analysis.",
tag: "Development",
},
]
export default function Component() {
const containerRef = useRef<HTMLDivElement>(null)
const [canScrollLeft, setCanScrollLeft] = useState(false)
const [canScrollRight, setCanScrollRight] = useState(true)
const { scrollXProgress } = useScroll({
container: containerRef,
})
const checkScrollButtons = () => {
if (containerRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = containerRef.current
setCanScrollLeft(scrollLeft > 0)
setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 1)
}
}
useEffect(() => {
checkScrollButtons()
const container = containerRef.current
if (container) {
container.addEventListener("scroll", checkScrollButtons)
return () => container.removeEventListener("scroll", checkScrollButtons)
}
}, [])
const scrollLeft = () => {
if (containerRef.current) {
containerRef.current.scrollBy({ left: -320, behavior: "smooth" })
}
}
const scrollRight = () => {
if (containerRef.current) {
containerRef.current.scrollBy({ left: 320, behavior: "smooth" })
}
}
return (
<div className="w-full bg-white">
{/* Header Section */}
<div className="max-w-6xl mx-auto px-6 pt-20 pb-16">
<div className="text-center mb-16">
<motion.div initial={{ opacity: 0, y: 20 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6 }}>
<h1 className="text-5xl md:text-6xl font-bold tracking-tight text-black mb-6">Our Journey</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto leading-relaxed">
From concept to launch. Every milestone that shaped our product.
</p>
</motion.div>
</div>
{/* Timeline Section */}
<div className="relative">
{/* Progress indicator */}
<div className="absolute top-0 left-16 right-16 h-px bg-gray-200 z-10">
<motion.div className="h-full bg-black origin-left" style={{ scaleX: scrollXProgress }} />
</div>
{/* Navigation */}
<Button
variant="outline"
size="icon"
className={`absolute left-0 top-1/2 -translate-y-1/2 z-20 bg-white border-gray-200 hover:bg-gray-50 shadow-sm ${
!canScrollLeft ? "opacity-40 cursor-not-allowed" : ""
}`}
onClick={scrollLeft}
disabled={!canScrollLeft}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="icon"
className={`absolute right-0 top-1/2 -translate-y-1/2 z-20 bg-white border-gray-200 hover:bg-gray-50 shadow-sm ${
!canScrollRight ? "opacity-40 cursor-not-allowed" : ""
}`}
onClick={scrollRight}
disabled={!canScrollRight}
>
<ChevronRight className="h-4 w-4" />
</Button>
{/* Timeline container */}
<div
ref={containerRef}
className="overflow-x-auto scrollbar-hide pt-8 pb-4 px-16"
style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
>
<div className="flex gap-8 relative min-w-max pb-8">
{/* Timeline line */}
<div className="absolute top-6 left-0 right-0 h-px bg-gray-200" />
{timelineEvents.map((event) => (
<motion.div
key={event.id}
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: index * 0.1, duration: 0.6 }}
className="relative flex-shrink-0 group"
>
{/* Timeline dot */}
<div className="absolute top-4 left-1/2 -translate-x-1/2 z-10">
<motion.div
className="w-4 h-4 bg-black rounded-full border-4 border-white shadow-sm"
whileHover={{ scale: 1.2 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }}
/>
</div>
{/* Event card */}
<motion.div
className="w-80 mt-12"
whileHover={{ y: -2 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
>
<div className="bg-white border border-gray-200 rounded-lg p-6 shadow-sm hover:shadow-md transition-all duration-300 group-hover:border-gray-300">
{/* Date and tag */}
<div className="flex items-center justify-between mb-4">
<span className="text-sm font-mono text-gray-500 tracking-wide">{event.date}</span>
<span className="text-xs px-2 py-1 bg-gray-100 text-gray-600 rounded-full font-medium">
{event.tag}
</span>
</div>
{/* Title */}
<h3 className="text-xl font-semibold text-black mb-3 tracking-tight">{event.title}</h3>
{/* Description */}
<p className="text-gray-600 leading-relaxed text-sm mb-4">{event.description}</p>
{/* Learn more link */}
<button className="inline-flex items-center text-sm text-black hover:text-gray-600 transition-colors group/link">
<span className="font-medium">Learn more</span>
<ArrowRight className="ml-1 h-3 w-3 transition-transform group-hover/link:translate-x-0.5" />
</button>
</div>
</motion.div>
</motion.div>
))}
</div>
</div>
</div>
{/* Stats section */}
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.8, duration: 0.6 }}
className="mt-20 pt-16 border-t border-gray-200"
>
<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center">
<div className="text-4xl font-bold text-black mb-2">7</div>
<div className="text-gray-600 text-sm font-medium tracking-wide uppercase">Major Milestones</div>
</div>
<div className="text-center">
<div className="text-4xl font-bold text-black mb-2">6</div>
<div className="text-gray-600 text-sm font-medium tracking-wide uppercase">Months to Launch</div>
</div>
<div className="text-center">
<div className="text-4xl font-bold text-black mb-2">1K+</div>
<div className="text-gray-600 text-sm font-medium tracking-wide uppercase">Active Users</div>
</div>
</div>
</motion.div>
</div>
</div>
)
}
Follow the installation steps above and start building with Horizontal Timeline.