A beautiful, interactive horizontal timeline for showcasing product features, releases, and roadmaps. Built with React, TypeScript, and Framer Motion.
- Drag to Navigate - Smooth horizontal scrolling with momentum physics
- Smart Card Layout - Automatic collision avoidance prevents overlapping
- Dynamic Month Widths - Optional mode expands dense months to fit more cards
- Search Bar - Optional search to find and navigate to features instantly
- Past & Future - Cards positioned above (future) or below (past) the timeline
- Focus Detection - Card closest to center automatically highlights
- Detail Modals - Click any card to see full details with media gallery
- Like/Dislike Voting - Built-in voting system (localStorage or API-ready)
- Dark & Light Themes - Toggle between themes with smooth transitions
- Back to Today - Floating button to reset view when scrolled away
- Fully Typed - Complete TypeScript support
- JSON-Driven - Feed data via URL or inline props
# Clone the repository
git clone https://github.com/iamboliver/dynamic-product-timeline.git
cd dynamic-product-timeline
# Install dependencies
npm install
# Start development server
npm run devimport { FeatureTimeline } from './components/FeatureTimeline';
function App() {
return <FeatureTimeline dataUrl="/features.json" />;
}import { FeatureTimeline } from './components/FeatureTimeline';
const features = [
{
id: '1',
title: 'Dark Mode',
description: 'Full dark theme support across the application.',
releaseDate: '2024-06-15',
status: 'released',
tags: ['UI', 'Accessibility'],
},
{
id: '2',
title: 'AI Assistant',
description: 'Intelligent assistant powered by machine learning.',
releaseDate: '2025-03-01',
status: 'planned',
highlight: true,
},
];
function App() {
return <FeatureTimeline features={features} />;
}Create a features.json file in your public folder:
[
{
"id": "unique-id",
"title": "Feature Name",
"description": "A detailed description of the feature.",
"releaseDate": "2025-01-15",
"status": "released",
"screenshots": [
"https://example.com/screenshot1.png",
"https://example.com/screenshot2.png"
],
"videos": [
"https://example.com/demo.mp4"
],
"tags": ["Category", "Type"],
"highlight": false
}
]Field Reference
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier |
title |
string | Yes | Feature name |
description |
string | Yes | Feature description |
releaseDate |
string | Yes | ISO 8601 date (YYYY-MM-DD) |
status |
string | Yes | released, beta, or planned |
screenshots |
string[] | No | Array of image URLs |
videos |
string[] | No | Array of video URLs |
tags |
string[] | No | Category tags |
highlight |
boolean | No | Emphasize this feature |
The timeline supports dark and light themes out of the box.
import { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import { FeatureTimeline } from './components/FeatureTimeline';
import { ThemeToggle } from './components/ThemeToggle';
import { darkTheme, lightTheme } from './utils/constants';
function App() {
const [isDark, setIsDark] = useState(true);
return (
<ThemeProvider theme={isDark ? darkTheme : lightTheme}>
<ThemeToggle isDark={isDark} onToggle={() => setIsDark(!isDark)} />
<FeatureTimeline dataUrl="/features.json" />
</ThemeProvider>
);
}import { TimelineTheme } from './types';
const customTheme: TimelineTheme = {
colors: {
background: '#0a0a0a',
backgroundElevated: '#141414',
backgroundSurface: '#1a1a1a',
primary: '#db0011', // Accent color
primaryGlow: 'rgba(219, 0, 17, 0.4)',
textPrimary: '#ffffff',
textSecondary: '#b3b3b3',
greyLight: '#b3b3b3',
greyMid: '#666666',
greyDark: '#333333',
statusReleased: '#22c55e', // Green
statusBeta: '#f59e0b', // Amber
statusPlanned: '#db0011', // Red
todayMarker: '#db0011',
},
spacing: {
cardBorderRadius: 20,
cardPadding: 16,
stemLength: 40,
baseYOffset: 100,
slotHeight: 120,
minCardSpacing: 200,
},
animation: {
dragMomentum: true,
dragElastic: 0.1,
focusTransitionDuration: 200,
entranceStaggerDelay: 100,
},
};The timeline includes a like/dislike voting system that works out of the box with localStorage.
To persist votes across users, update src/services/voteService.ts:
// Set these URLs to enable API mode
const VOTES_API_URL = 'https://api.example.com/votes'; // GET: returns VotesMap
const VOTE_SUBMIT_URL = 'https://api.example.com/vote'; // POST: submit voteExpected API format:
// GET /votes response
{
"feature-id-1": { "likes": 42, "dislikes": 3 },
"feature-id-2": { "likes": 15, "dislikes": 8 }
}
// POST /vote request body
{
"featureId": "feature-id-1",
"voteType": "like", // or "dislike"
"previousVote": "dislike" // optional, if changing vote
}
// POST /vote response
{ "likes": 43, "dislikes": 2 }interface FeatureTimelineProps {
dataUrl?: string; // URL to fetch features JSON
features?: Feature[]; // Inline feature data
today?: Date; // Override "today" (default: new Date())
pxPerDay?: number; // Pixels per day spacing (default: 12)
className?: string; // Additional CSS class
dynamicMonthWidths?: boolean; // Expand dense months to fit cards (default: false)
searchEnabled?: boolean; // Show search bar to find features (default: false)
}By default, the timeline uses a linear scale where each day has equal width. When you have many features in the same month, cards may overlap.
Enable dynamicMonthWidths to automatically expand months with more features:
<FeatureTimeline dataUrl="/features.json" dynamicMonthWidths />With this enabled:
- Months with more features become wider
- Cards within dense months spread out horizontally
- The date label updates correctly when dragging through variable-width months
Enable a search bar in the top-left corner to quickly find and navigate to features:
<FeatureTimeline dataUrl="/features.json" searchEnabled />With this enabled:
- Search box appears in the top-left corner
- Searches feature titles, descriptions, and tags
- Shows up to 5 matching results in a dropdown
- Clicking a result smoothly scrolls to that card and opens its modal
src/
βββ components/
β βββ FeatureTimeline/
β β βββ index.ts # Public exports
β β βββ FeatureTimeline.tsx # Main container
β β βββ TimelineAxis.tsx # Horizontal line + ticks
β β βββ TodayMarker.tsx # Center marker
β β βββ FeatureCardsLayer.tsx # Card positioning
β β βββ FeatureCard.tsx # Individual card
β β βββ ConnectorStem.tsx # Card-to-axis connector
β β βββ FeatureModal.tsx # Detail modal
β β βββ styles.ts # Styled components
β βββ ThemeToggle.tsx # Dark/light toggle
βββ hooks/
β βββ useFeatureData.ts # Data loading & processing
β βββ useFocusedFeature.ts # Focus detection
β βββ useVotes.ts # Voting state
βββ services/
β βββ voteService.ts # Vote API layer
βββ utils/
β βββ timeScale.ts # Date-to-pixel math
β βββ collisionAvoidance.ts # Card staggering
β βββ constants.ts # Theme defaults
βββ types/
β βββ index.ts # TypeScript interfaces
βββ App.tsx
βββ main.tsx
# Start dev server
npm run dev
# Type checking
npm run build
# Preview production build
npm run previewx = 0represents today- Past dates β negative x values
- Future dates β positive x values
- Formula:
x = daysDifference Γ pxPerDay
- Hemisphere: Past features go below the line, future features go above
- Collision Avoidance: Cards within
minCardSpacingpixels are vertically staggered - Focus Detection: Card closest to viewport center receives focus styling
- Uses Framer Motion's pan gesture system
- Spring physics for smooth deceleration
- Bounded to prevent scrolling past first/last feature
- Node.js 18+
- React 18+
- Modern browser with ES2020 support
MIT License - feel free to use this in your own projects!
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request


