Building an Interactive Timeline Component

By Claude Sonnet 4.5*AI-generated based on summary of work done on component·  January 31, 2026
🏷️ Tags:devreactcomponentssvgdesign

Deep dive into creating a custom SVG-based timeline component with hover interactions, dynamic positioning, and responsive year labels

Building an Interactive Timeline Component

I built a custom timeline component for my about page to visualize my development journey from 2019 to 2026. What started as a simple idea turned into an interesting exploration of SVG manipulation, React state management, and spatial UI interactions. Here's how I built it and the bugs I encountered along the way.

What It Does

The timeline shows project milestones chronologically with these features:

Architecture Overview

The component is built with React, Emotion CSS, and SVG. The key architectural decisions:

SVG-Based Timeline

I chose SVG over HTML/CSS because it gives precise control over positioning and allows tick marks to extend dynamically on hover without layout shifts. The viewBox is set to -40 65 290 1490to accommodate year labels on the left while keeping the timeline content properly bounded.

.ts
// Timeline spans 2019-2026, positions calculated from dates
const getYPosition = (year: number, month: number): number => {
  const startYear = 2019;
  const startMonth = 1;
  const totalMonths = (2026 - 2019) * 12;
  const timelineHeight = 1490;
  const monthsFromStart = (year - startYear) * 12 + (month - startMonth);
  return 1555 - (monthsFromStart / totalMonths) * timelineHeight;
};

Every milestone gets a Y position calculated from its actual date. This makes the spacing proportional to time rather than arbitrary.

Hover Interaction System

I use React state to track which timeline position is being hovered. Each tick mark that has events gets an invisible 30x30px rect as a hover target:

.ts
const [hoveredPosition, setHoveredPosition] = useState<number | null>(null);

// In the tick mark rendering:
if (hasEvent) {
  const isHovered = hoveredPosition !== null && Math.abs(hoveredPosition - y) < 5;
  const finalX2 = isHovered ? 200 : x2;
  
  // Tick mark extends from x1 to finalX2 when hovered
  ticks.push(
    <line x1={x1} y1={y} x2={finalX2} y2={y} ... />
  );
}

When hovered, the tick mark extends from its normal length (12-36px) all the way to x=200, creating a visual connection to the card that appears at x=205.

Multi-Card Stacking

Multiple projects in the same month need to stack without overlapping. I calculate vertical offsets based on how many cards share the same Y position:

.ts
// In card rendering:
const cardsAtSamePosition = devMilestones.filter(m => Math.abs(m.y - item.y) < 5);
const indexAtPosition = cardsAtSamePosition.findIndex(m => m.title === item.title);
const verticalOffset = indexAtPosition * 240; // 240px spacing between cards

// Position with offset applied
style={{ 
  top: `${item.y + verticalOffset}px`, 
  left: '205px',
  opacity: isHovered ? 1 : 0,
}}

Major Bugs and Fixes

Bug #1: Overlapping Tick Mark Lines

Initially, I was drawing the base tick mark AND an extension line on hover, which created a visible artifact where they met. The fix was to make the tick mark's x2 dynamic - it extends all the way to x=200 when hovered instead of drawing two separate lines.

Bug #2: Year Labels Cut Off

The year labels (26, 25, 24...) were positioned at x=-5 but the viewBox started at x=0, so they were being clipped. I adjusted the viewBox to start at x=-40, giving 40px of space for the labels on the left.

Bug #3: Massive Top/Bottom Padding

The SVG had huge empty space at top and bottom because the viewBox was 0-1620 but the actual timeline content only spanned y=65 to y=1555. Fixed by adjusting viewBox to start at y=65 with height=1490, and matching the sideStyles height to 1490px.

Bug #4: Cards Not Spatially Related to Tick Marks

At one point the SVG was 250px wide but the grid column was only 60px, causing a misalignment between where cards appeared and where tick marks extended. The SVG width, viewBox width, and container width all need to be coordinated.

Design Evolution

The component went through several design iterations:

Tech Stack Details

The component uses:

Code Organization

The file structure follows a pattern I use across components:

Performance Considerations

With 14 milestones and tick marks generated for every month from 2019-2026 (84 months), there are ~100 SVG elements being rendered. The component performs well because:

What I'd Do Differently

If I were to rebuild this component from scratch with what I know now:

Conclusion

This component demonstrates that custom timeline UIs don't require heavy libraries - with SVG, React, and some geometric calculations, you can build something both functional and visually interesting. The key is understanding coordinate systems, managing state cleanly, and iterating on the design based on what works visually.

The full component code is available in my site's repository at src/components/Timeline.tsx.