Skip to main content

Timeline Component

The Timeline component displays chronological content with a scroll-based progress animation. It’s perfect for showcasing education history, work experience, or any time-ordered information with an elegant vertical layout.

Features

  • Scroll-based progress animation
  • Animated gradient progress line
  • Responsive layout (mobile/desktop)
  • Sticky titles on desktop view
  • Smooth opacity transitions
  • TypeScript support
  • Framer Motion animations
  • Customizable content with React nodes

Source Code Location

src/components/ui/timeline.tsx

Dependencies

import {
  useMotionValueEvent,
  useScroll,
  useTransform,
  motion,
} from "motion/react";
import React, { useEffect, useRef, useState } from "react";

Props

data
TimelineEntry[]
required
Array of timeline entries to display

TimelineEntry Interface

interface TimelineEntry {
  title: string;        // The title/year/date for the entry
  content: React.ReactNode;  // Any React content (JSX, components, etc.)
}

Usage

Basic Example

import { Timeline } from './components/ui/timeline';

const data = [
  {
    title: "2024",
    content: (
      <div>
        <h2>Software Engineer</h2>
        <p>Worked on amazing projects</p>
      </div>
    ),
  },
  {
    title: "2023",
    content: (
      <div>
        <h2>Junior Developer</h2>
        <p>Started my career in web development</p>
      </div>
    ),
  },
];

function Experience() {
  return <Timeline data={data} />;
}

Real-World Example from About Page

From src/pages/About.jsx:12-35:
const formacion = t("about.education", { returnObjects: true }).map(
  (item) => ({
    title: item.year,
    content: (
      <div className="flex flex-row w-full gap-4 items-center">
        <div className="flex-1 text-left">
          <h2 className="text-lg font-semibold text-subtitle">
            {item.title}
          </h2>
          <p className="text-sm font-normal text-text">{item.school}</p>
        </div>
        <div className="flex justify-end">
          <div className="h-32 w-32 rounded-lg overflow-hidden drop-shadow-[0_0_25px_rgba(168,85,247,0.8)] dark:drop-shadow-[0_0_25px_rgba(168,85,247,0.3)] bg-white/0.1 dark:bg-white backdrop-blur-md">
            <img
              src={item.src}
              alt={item.alt}
              className="w-full h-full object-contain"
            />
          </div>
        </div>
      </div>
    ),
  }),
);

<Timeline data={formacion} />

Component Implementation

State Management

const ref = useRef<HTMLDivElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState(0);

Height Calculation

The component calculates total height on mount:
useEffect(() => {
  if (ref.current) {
    const rect = ref.current.getBoundingClientRect();
    setHeight(rect.height);
  }
}, [ref]);

Scroll Tracking

const { scrollYProgress } = useScroll({
  target: containerRef,
  offset: ["start 10%", "end 50%"],
});
Scroll offset parameters:
  • "start 10%": Animation starts when timeline top reaches 10% of viewport
  • "end 50%": Animation completes when timeline bottom reaches 50% of viewport

Transform Animations

const heightTransform = useTransform(scrollYProgress, [0, 1], [0, height]);
const opacityTransform = useTransform(scrollYProgress, [0, 0.1], [0, 1]);
  • heightTransform: Progress line grows from 0 to full height
  • opacityTransform: Quick fade-in during first 10% of scroll

Layout Structure

Desktop Layout

On desktop (md breakpoint and above):
<div className="flex justify-start pt-10 md:pt-20 md:gap-10">
  {/* Sticky title column */}
  <div className="sticky flex flex-col md:flex-row z-40 items-center top-40 self-start max-w-xs lg:max-w-sm md:w-full">
    <div className="h-10 absolute left-3 md:left-3 w-10 rounded-full bg-neutral-300 dark:bg-neutral-900 flex items-center justify-center">
      <div className="h-4 w-4 rounded-full bg-neutral-100 dark:bg-neutral-600 border border-neutral-300 dark:border-neutral-900 p-2" />
    </div>
    <h3 className="hidden md:block text-xl md:pl-20 md:text-3xl font-bold text-neutral-500 dark:text-neutral-500">
      {item.title}
    </h3>
  </div>

  {/* Content column */}
  <div className="relative pl-20 pr-4 md:pl-4 w-full">
    {item.content}
  </div>
</div>

Mobile Layout

On mobile, the title appears above content:
<h3 className="md:hidden block text-2xl mb-4 text-left font-bold text-neutral-500 dark:text-neutral-400">
  {item.title}
</h3>

Progress Line

The vertical progress line uses a gradient background with animated fill:
<div
  style={{ height: height + "px" }}
  className="absolute md:left-8 left-8 top-0 overflow-hidden w-[2px] bg-[linear-gradient(to_bottom,var(--tw-gradient-stops))] from-transparent from-[0%] via-neutral-200 dark:via-neutral-700 to-transparent to-[99%] [mask-image:linear-gradient(to_bottom,transparent_0%,black_10%,black_90%,transparent_100%)]"
>
  <motion.div
    style={{
      height: heightTransform,
      opacity: opacityTransform,
    }}
    className="absolute inset-x-0 top-0 w-[2px] bg-gradient-to-t from-purple-600 via-violet-300 to-transparent from-[0%] via-[10%] rounded-full"
  />
</div>

Progress Line Features

  • Width: 2px thin line
  • Base gradient: Neutral colors that fade at top/bottom
  • Mask: Creates soft fade at edges
  • Animated fill: Purple-violet gradient that grows with scroll
  • Position: Absolute positioned at left edge

Timeline Nodes

Each entry has a circular node: Outer circle:
  • 40px diameter (h-10 w-10)
  • bg-neutral-300 dark:bg-neutral-900
Inner circle:
  • 16px diameter (h-4 w-4)
  • bg-neutral-100 dark:bg-neutral-600
  • Border matches outer circle color

Styling Classes

Container

  • w-full font-sans md:px-10: Full width with responsive padding
  • max-w-7xl mx-auto pb-20: Centered with max width and bottom spacing

Entry Spacing

  • pt-10 md:pt-20: Responsive top padding (40px mobile, 80px desktop)
  • md:gap-10: 40px gap between title and content on desktop

Title Positioning

  • sticky top-40: Sticks to viewport 160px from top while scrolling
  • self-start: Aligns to start of flex container
  • z-40: Ensures it appears above other content

Responsive Breakpoints

Mobile (< 768px)

  • Title appears above content
  • Narrower spacing
  • Smaller font sizes

Desktop (≥ 768px)

  • Sticky title column
  • Side-by-side layout
  • Larger typography

Animation Performance

The component uses Framer Motion’s useTransform which:
  • Runs on the GPU for smooth 60fps animations
  • Only recalculates on scroll events
  • Efficiently updates CSS transform properties
  • No JavaScript animation loops

Usage in About Page

From src/pages/About.jsx:116:
<section className="relative w-full">
  <h1 className="text-3xl md:text-4xl flex items-center justify-center gap-2 mb-8 text-title font-bold">
    <IconBooks className="w-8 h-8 text-violet-500" />
    {t("about.sections.education")}
  </h1>
  <Timeline data={formacion} />
</section>

Customization Tips

Change Progress Colors

Modify the gradient in the motion.div:
className="bg-gradient-to-t from-blue-600 via-cyan-300 to-transparent"

Adjust Scroll Offset

const { scrollYProgress } = useScroll({
  target: containerRef,
  offset: ["start 20%", "end 60%"],  // Custom offsets
});

Modify Node Appearance

<div className="h-12 w-12 rounded-full bg-violet-500">  {/* Custom size/color */}
  <div className="h-6 w-6 rounded-full bg-white" />
</div>

Build docs developers (and LLMs) love