Animated Timeline
Preview
Jan 2023
Project Kickoff
The project officially started.
Mar 2023
Alpha Release
First version released to a closed group.
Jun 2023
Beta Launch
Public beta available for testing.
Sep 2023
Official Launch
Version 1.0 is live!
Code
"use client";
import { motion, useScroll, useTransform } from "framer-motion";
import { useRef, type FC, type ReactNode } from "react";
import { cn } from "@/lib/utils";
interface TimelineItem {
title: string;
description: string;
date: string;
icon?: ReactNode;
}
interface AnimatedTimelineProps {
items: TimelineItem[];
className?: string;
}
const AnimatedTimeline: FC<AnimatedTimelineProps> = ({ items, className }) => {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const pathProgress = useTransform(scrollYProgress, [0, 0.95], [0, 1]);
return (
<div ref={ref} className={cn("relative w-full max-w-md mx-auto py-8", className)}>
<svg
width="2"
height="100%"
viewBox="0 0 2 1000"
className="absolute left-[15px] top-0 h-full"
preserveAspectRatio="none"
>
<motion.path
d="M 1 0 V 1000"
stroke="rgba(255, 255, 255, 0.2)"
strokeWidth="2"
style={{ pathLength: pathProgress }}
initial={{ pathLength: 0 }}
/>
</svg>
<div className="space-y-12">
{items.map((item, index) => (
<TimelineItemComponent key={index} item={item} />
))}
</div>
</div>
);
};
const TimelineItemComponent: FC<{ item: TimelineItem }> = ({ item }) => {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end end"],
});
const scale = useTransform(scrollYProgress, [0, 1], [0.5, 1]);
const opacity = useTransform(scrollYProgress, [0, 1], [0.3, 1]);
return (
<motion.div
ref={ref}
style={{ scale, opacity }}
className="flex items-start gap-4"
>
<div className="relative z-10">
<div className="w-8 h-8 rounded-full bg-purple-600 flex items-center justify-center ring-8 ring-black">
{item.icon || <div className="w-3 h-3 rounded-full bg-white" />}
</div>
</div>
<div className="flex-1 pt-1">
<p className="text-sm text-gray-400">{item.date}</p>
<h3 className="text-lg font-semibold text-white mt-1">{item.title}</h3>
<p className="text-gray-300 mt-2">{item.description}</p>
</div>
</motion.div>
);
};
export default AnimatedTimeline;