Розробка Timeline-візуалізацій для веб-сайтів
Timeline-візуалізації відображають події у часі: історія компанії, audit-log, біографія, лента активності проекту. Можуть бути вертикальними (лента новин) та горизонтальними (історичний таймлайн).
Бібліотеки
- vis-timeline — інтерактивний горизонтальний таймлайн з групами, drag-drop
- react-chrono — красиві вертикальні та горизонтальні лінії
- Framer Motion — користувацька анімація для вертикального таймлайну
vis-timeline — інтерактивний горизонтальний
npm install vis-timeline vis-data
import { useEffect, useRef } from 'react';
import { Timeline, DataSet } from 'vis-timeline/standalone';
import 'vis-timeline/styles/vis-timeline-graph2d.css';
function ProjectTimeline({ events, groups }) {
const containerRef = useRef<HTMLDivElement>(null);
const timelineRef = useRef<Timeline>();
useEffect(() => {
if (!containerRef.current) return;
const items = new DataSet(events.map(e => ({
id: e.id,
group: e.groupId,
content: `<div class="timeline-item">${e.title}</div>`,
start: e.startDate,
end: e.endDate,
className: `status-${e.status}`
})));
const groupsDS = new DataSet(groups.map(g => ({
id: g.id,
content: g.name
})));
const timeline = new Timeline(containerRef.current, items, groupsDS, {
start: new Date(Date.now() - 7 * 24 * 3600000),
end: new Date(Date.now() + 30 * 24 * 3600000),
height: '400px',
locale: 'uk',
groupOrder: 'id',
zoomMin: 1000 * 60 * 60 * 24,
zoomMax: 1000 * 60 * 60 * 24 * 365
});
timeline.on('select', ({ items: selectedIds }) => {
if (selectedIds.length > 0) {
const item = events.find(e => e.id === selectedIds[0]);
onEventSelect(item);
}
});
timelineRef.current = timeline;
return () => timeline.destroy();
}, []);
return <div ref={containerRef} />;
}
Вертикальний Timeline (CSS + React)
import { motion } from 'framer-motion';
interface TimelineEvent {
id: string;
date: string;
title: string;
description: string;
type: 'milestone' | 'update' | 'issue';
actor?: string;
}
function VerticalTimeline({ events }: { events: TimelineEvent[] }) {
const icons = {
milestone: '🎯',
update: '📝',
issue: '⚠️'
};
return (
<div className="relative">
<div className="absolute left-8 top-0 bottom-0 w-0.5 bg-gray-200" />
<div className="space-y-6 pl-20">
{events.map((event, index) => (
<motion.div
key={event.id}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: index * 0.05 }}
className="relative"
>
<div className="absolute -left-12 w-8 h-8 rounded-full bg-white border-2 border-blue-300 flex items-center justify-center text-sm">
{icons[event.type]}
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4 shadow-sm">
<div className="flex items-start justify-between mb-1">
<h4 className="font-semibold text-gray-800">{event.title}</h4>
<time className="text-xs text-gray-500 ml-4 shrink-0">
{formatDate(event.date)}
</time>
</div>
<p className="text-sm text-gray-600">{event.description}</p>
{event.actor && (
<p className="text-xs text-gray-400 mt-2">{event.actor}</p>
)}
</div>
</motion.div>
))}
</div>
</div>
);
}
react-chrono
import { Chrono } from 'react-chrono';
function CompanyHistory({ milestones }) {
const items = milestones.map(m => ({
title: m.year.toString(),
cardTitle: m.title,
cardDetailedText: m.description,
media: m.imageUrl ? {
type: 'IMAGE',
source: { url: m.imageUrl }
} : undefined
}));
return (
<Chrono
items={items}
mode="VERTICAL_ALTERNATING"
theme={{
primary: '#3b82f6',
secondary: '#eff6ff',
cardBgColor: '#ffffff',
titleColor: '#374151'
}}
cardHeight={200}
useReadMore={false}
/>
);
}
Часові межи
Вертикальний Timeline з анімаціями — 2–3 дні. vis-timeline з групами та drag-drop — 4–6 днів.







