VibeSnip/

Horizontal Timeline

An interactive horizontal timeline with smooth scrolling, animated progress tracking, and responsive design. Perfect for showcasing project milestones, company history, or any chronological events.

layoutnavigationdataprogressinteractive
Layout
Component Preview
Interactive preview of the Horizontal Timeline component

Our Journey

From concept to launch. Every milestone that shaped our product.

2024.01Launch

Project Kickoff

Initial planning and team formation with ambitious goals.

2024.02Development

First Prototype

Built and tested our first working prototype.

2024.03Research

User Research

Conducted extensive user testing with 50+ participants.

2024.04Launch

Beta Launch

Soft launch to select beta users for feedback.

2024.05Milestone

Public Release

Official launch with marketing campaign.

2024.06Growth

Growth Phase

Rapid user acquisition and feature expansion.

6
Milestones
6
Months
1K+
Users
Install dependencies
Run this command to install all required dependencies
npm i motion clsx tailwind-merge lucide-react
Add util file
lib/utils.ts
import { ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
 
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
Copy the source code
components/ui/horizontal-timeline.tsx
"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>
  )
}

Ready to use this component?

Follow the installation steps above and start building with Horizontal Timeline.