Velocity UI
Loading…
Menu

Animated Timeline

A vertical timeline with a glowing beam that fills as you scroll.

Installation

Add this component to your project using the CLI:

terminal
npx vui-registry-cli-v1 add timeline

Source Code

timeline.tsx
'use client'

import React, { useRef, useState, useEffect } from 'react'
import { motion, useScroll, useSpring, useTransform } from 'framer-motion'
import { cn } from '@/lib/utils'

interface TimelineProps {
  items: {
    title: string
    date: string
    description: string
    icon?: React.ReactNode
  }[]
  className?: string
}

export function Timeline({ items, className }: TimelineProps) {
  const containerRef = useRef<HTMLDivElement>(null)
  const { scrollYProgress } = useScroll({
    target: containerRef,
    offset: ['start end', 'end start'],
  })
  
  const scaleY = useSpring(scrollYProgress, {
    stiffness: 100,
    damping: 30,
    restDelta: 0.001
  })

  return (
    <div ref={containerRef} className={cn('relative w-full max-w-2xl mx-auto py-20', className)}>
      {/* Central Line */}
      <div className="absolute left-[20px] md:left-1/2 top-0 bottom-0 w-[2px] bg-border md:-translate-x-1/2" />
      
      {/* Animated Filling Line */}
      <motion.div
        style={{ scaleY }}
        className="absolute left-[20px] md:left-1/2 top-0 bottom-0 w-[2px] bg-gradient-to-b from-primary via-primary/50 to-transparent origin-top md:-translate-x-1/2 z-10"
      />

      <div className="space-y-12">
        {items.map((item, index) => (
          <TimelineItem key={index} item={item} index={index} />
        ))}
      </div>
    </div>
  )
}

function TimelineItem({ item, index }: { item: any; index: number }) {
  const isEven = index % 2 === 0
  
  return (
    <motion.div
      initial={{ opacity: 0, y: 50 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: '-100px' }}
      transition={{ duration: 0.5, delay: index * 0.1 }}
      className={cn(
        'relative flex items-center md:justify-between',
        isEven ? 'md:flex-row-reverse' : ''
      )}
    >
      {/* Content */}
      <div className="w-[calc(100%-60px)] md:w-[45%] pl-12 md:pl-0">
        <div className="p-6 rounded-2xl border border-border/50 bg-background/50 backdrop-blur-sm hover:border-primary/50 transition-colors shadow-sm">
          <div className="flex items-center justify-between mb-2">
            <h3 className="font-semibold text-lg">{item.title}</h3>
            <span className="text-xs font-mono text-muted-foreground px-2 py-1 rounded bg-muted/50">
              {item.date}
            </span>
          </div>
          <p className="text-sm text-muted-foreground leading-relaxed">
            {item.description}
          </p>
        </div>
      </div>

      {/* Node */}
      <div className="absolute left-[20px] md:left-1/2 w-4 h-4 rounded-full bg-background border-2 border-primary z-20 md:-translate-x-1/2 shadow-[0_0_10px_rgba(var(--primary-rgb),0.5)]">
        <div className="absolute inset-0 rounded-full bg-primary/20 animate-ping" />
      </div>

      {/* Empty Space for layout balance */}
      <div className="hidden md:block w-[45%]" />
    </motion.div>
  )
}

export function TimelinePreview() {
  const items = [
    {
      title: 'Project Inception',
      date: 'Jan 2024',
      description: 'Initial brainstorming and feasibility study for the new platform architecture.',
    },
    {
      title: 'Design System',
      date: 'Mar 2024',
      description: 'Established the core design tokens, typography, and component library.',
    },
    {
      title: 'Beta Launch',
      date: 'Jun 2024',
      description: 'Released the MVP to a closed group of beta testers for feedback.',
    },
    {
      title: 'Global Release',
      date: 'Aug 2024',
      description: 'Official public launch with full feature set and documentation.',
    },
  ]

  return (
    <div className="w-full">
      <Timeline items={items} />
    </div>
  )
}

Dependencies

  • framer-motion: latest
  • clsx: latest
  • tailwind-merge: latest

Props

Component property reference.

NameTypeDefaultDescription
itemsArray<{ title: string; date: string; description: string; icon?: React.ReactNode }>[]Timeline entries to render.
classNamestringundefinedAdditional CSS classes.
Context Worth Keeping In Orbit

Most components here are inspired by outstanding libraries and creators in the ecosystem. I don’t claim to be the original author — this is my space for learning, rebuilding, and understanding great work at a deeper level.

I’m still a student of the craft, constantly studying the best and translating what I learn through my own perspective. Every piece reflects curiosity, respect for the community, and small creative touches that feel true to me.

I’ve done my best to credit inspirations properly. If anything is missing or inaccurate, I truly appreciate a message so it can be corrected with care.