Animated Timeline
The Animated Timeline component is an interactive, visually appealing timeline that responds to user interaction. Built with Framer Motion and React, this component highlights key events or milestones in a vertical timeline structure.When a user hovers over a specific timeline item, the associated circular dot and all previous dots in the sequence turn green, indicating progression. The dot size also enlarges slightly, enhancing the focus on the current event. The component offers a sleek and smooth animation experience, perfect for showcasing chronological steps, milestones, or achievements in an engaging and user-friendly manner.This component is highly customizable, allowing easy modifications to the timeline content and styling, making it suitable for diverse applications such as resumes, project timelines, or product roadmaps.
Installation
Install dependencies
npm install framer-motion lucide-react
Run the following command
It will create a new file animatedtimeline.tsx
inside the components/animata/progress
directory.
mkdir -p components/animata/progress && touch components/animata/progress/animatedtimeline.tsx
Paste the code
Open the newly created file and paste the following code:
"use client";
import React, { useState } from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
export interface TimelineEvent {
id: string;
title: string;
description?: string;
date?: string;
[key: string]: unknown; // Allow additional custom fields
}
interface TimelineItemProps {
event: TimelineEvent;
isActive: boolean;
isFirst: boolean;
isLast: boolean;
onHover: (index: number | null) => void;
index: number;
activeIndex: number | null;
styles: TimelineStyles;
customRender?: (event: TimelineEvent) => React.ReactNode;
}
interface TimelineStyles {
lineColor: string;
activeLineColor: string;
dotColor: string;
activeDotColor: string;
dotSize: string;
titleColor: string;
descriptionColor: string;
dateColor: string;
}
const TimelineItem: React.FC<TimelineItemProps> = ({
event,
isActive,
isLast,
onHover,
index,
activeIndex,
styles,
customRender,
}) => {
const fillDelay = activeIndex !== null ? Math.max(0, (index - 1) * 0.1) : 0;
const fillDuration = activeIndex !== null ? Math.max(0.2, 0.5 - index * 0.1) : 0.5;
return (
<motion.div
className="flex last:mb-0"
onHoverStart={() => onHover(index)}
onHoverEnd={() => onHover(null)}
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
<div className="relative mr-4 flex flex-col items-center">
<div
className={`absolute ${isLast ? "hidden" : "block"} bottom-0 top-0 w-1`}
style={{ backgroundColor: styles.lineColor }}
>
<motion.div
className="w-full origin-top"
initial={{ scaleY: 0 }}
animate={{ scaleY: isActive ? 1 : 0 }}
transition={{ duration: fillDuration, delay: fillDelay }}
style={{ height: "100%", backgroundColor: styles.activeLineColor }}
/>
</div>
<motion.div
className="relative z-10 rounded-full border-4"
style={{
width: styles.dotSize,
height: styles.dotSize,
borderColor: isActive ? styles.activeDotColor : styles.dotColor,
backgroundColor: isActive ? styles.activeDotColor : "Background",
}}
animate={{
scale: isActive ? 1.2 : 1,
backgroundColor: isActive ? styles.activeDotColor : "Background",
borderColor: isActive ? styles.activeDotColor : styles.dotColor,
}}
transition={{ duration: fillDuration, delay: fillDelay }}
/>
</div>
<div className={cn("flex-grow leading-5", !isLast && "mb-3")}>
{customRender ? (
customRender(event)
) : (
<>
<h3 className="text-lg font-semibold" style={{ color: styles.titleColor }}>
{event.title}
</h3>
<p style={{ color: styles.descriptionColor }}>{event.description}</p>
<span className="text-sm" style={{ color: styles.dateColor }}>
{event.date}
</span>
</>
)}
</div>
</motion.div>
);
};
interface AnimatedTimelineProps {
events: TimelineEvent[];
className?: string;
styles?: Partial<TimelineStyles>;
customEventRender?: (event: TimelineEvent) => React.ReactNode;
onEventHover?: (event: TimelineEvent | null) => void;
onEventClick?: (event: TimelineEvent) => void;
initialActiveIndex?: number;
}
const defaultStyles: TimelineStyles = {
lineColor: "#d1d5db",
activeLineColor: "#22c55e",
dotColor: "#d1d5db",
activeDotColor: "#22c55e",
dotSize: "1.5rem",
titleColor: "inherit",
descriptionColor: "inherit",
dateColor: "inherit",
};
export function AnimatedTimeline({
events,
className = "",
styles: customStyles = {},
customEventRender,
onEventHover,
onEventClick,
initialActiveIndex,
}: AnimatedTimelineProps) {
const [activeIndex, setActiveIndex] = useState<number | null>(initialActiveIndex ?? null);
const styles = { ...defaultStyles, ...customStyles };
const handleHover = (index: number | null) => {
setActiveIndex(index);
onEventHover?.(index !== null ? events[index] : null);
};
return (
<div className={`relative py-4 ${className}`}>
{events.map((event, index) => (
<div key={event.id} onClick={() => onEventClick?.(event)}>
<TimelineItem
event={event}
isActive={activeIndex !== null && index <= activeIndex}
isFirst={index === 0}
isLast={index === events.length - 1}
onHover={handleHover}
index={index}
activeIndex={activeIndex}
styles={styles}
customRender={customEventRender}
/>
</div>
))}
</div>
);
}
interface AnimatedTimelinePageProps {
events?: TimelineEvent[];
title?: string;
containerClassName?: string;
timelineStyles?: Partial<TimelineStyles>;
customEventRender?: (events: TimelineEvent) => React.ReactNode;
onEventHover?: (events: TimelineEvent | null) => void;
onEventClick?: (events: TimelineEvent) => void;
initialActiveIndex?: number;
}
export default function AnimatedTimelinePage({
events,
title,
containerClassName,
timelineStyles,
customEventRender,
onEventHover,
onEventClick,
initialActiveIndex,
}: AnimatedTimelinePageProps) {
const DefaultEvents = [
{ id: "1", title: "Event 1", description: "Description 1", date: "2023-01-01" },
{ id: "2", title: "Event 2", description: "Description 2", date: "2023-02-01" },
{ id: "3", title: "Event 3", description: "Description 3", date: "2023-03-01" },
];
const defaultTitle = "Timeline";
return (
<div
className={cn(
"container mx-auto rounded-lg bg-background px-8 pt-6 text-foreground",
containerClassName,
)}
>
<h1 className="text-2xl font-bold">{title || defaultTitle}</h1>
<AnimatedTimeline
events={events || DefaultEvents}
className="max-w-2xl"
styles={timelineStyles}
customEventRender={customEventRender}
onEventHover={onEventHover}
onEventClick={onEventClick}
initialActiveIndex={initialActiveIndex}
/>
</div>
);
}
Credits
Built by Vishal Kumar