A vertical event timeline inspired by Vercel. Shows system and user actions with typed icons, actor avatars, and day grouping. Perfect for settings pages and admin dashboards.
// Dependencies: react ^18, lucide-react
import React from 'react';
import type { LucideIcon } from 'lucide-react';
import {
Rocket, KeyRound, UserPlus, Shield,
Settings, Trash2, GitMerge, AlertTriangle,
} from 'lucide-react';
type AuditEvent = {
id: string;
icon: LucideIcon;
iconColor: string;
actor: string;
actorColor: string;
actorInitials: string;
description: string;
timestamp: Date;
};
function formatRelative(date: Date): string {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
const diffDays = Math.floor(diffHours / 24);
if (diffMins < 1) return 'Just now';
if (diffMins < 60) return `${diffMins}m ago`;
if (diffHours < 24) return `${diffHours}h ago`;
if (diffDays === 1) return 'Yesterday';
return `${diffDays}d ago`;
}
function groupByDay(events: AuditEvent[]): { label: string; events: AuditEvent[] }[] {
const now = new Date();
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
const yesterdayStart = todayStart - 86400000;
const groups: Record<string, AuditEvent[]> = {};
const order: string[] = [];
for (const e of events) {
const t = e.timestamp.getTime();
let label: string;
if (t >= todayStart) label = 'Today';
else if (t >= yesterdayStart) label = 'Yesterday';
else label = e.timestamp.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
if (!groups[label]) { groups[label] = []; order.push(label); }
groups[label].push(e);
}
return order.map((label) => ({ label, events: groups[label] }));
}
type Props = { events: AuditEvent[] };
export function AuditLogTimeline({ events }: Props) {
const groups = groupByDay(events);
return (
<div className="flex flex-col h-full overflow-hidden bg-[#0a0a0a] border border-[#1a1a1a] rounded-xl">
<div className="flex items-center justify-between px-4 py-3 border-b border-[#1a1a1a] shrink-0">
<span className="text-sm font-semibold text-white">Audit Log</span>
<span className="text-xs text-zinc-600">{events.length} events</span>
</div>
<div className="flex-1 overflow-y-auto px-4 py-3">
{groups.map((group) => (
<div key={group.label} className="mb-4">
<div className="flex items-center gap-2 mb-3">
<span className="text-[11px] font-medium text-zinc-500 uppercase tracking-wide">
{group.label}
</span>
<div className="flex-1 h-px bg-[#1a1a1a]" />
</div>
<div className="relative">
<div className="absolute left-[13px] top-0 bottom-0 w-px bg-[#1e1e1e]" />
<div className="flex flex-col">
{group.events.map((event) => (
<EventRow key={event.id} event={event} />
))}
</div>
</div>
</div>
))}
</div>
</div>
);
}
function EventRow({ event }: { event: AuditEvent }) {
const Icon = event.icon;
return (
<div className="flex items-start gap-3 py-2">
<div
className="relative z-10 w-7 h-7 rounded-full border flex items-center justify-center shrink-0"
style={{ backgroundColor: `${event.iconColor}18`, borderColor: `${event.iconColor}30` }}
>
<Icon className="w-3 h-3" style={{ color: event.iconColor }} />
</div>
<div className="flex-1 min-w-0 pt-1">
<div className="flex items-center gap-2 flex-wrap">
<div
className="w-4 h-4 rounded-full flex items-center justify-center text-white text-[9px] font-bold shrink-0"
style={{ backgroundColor: event.actorColor }}
>
{event.actorInitials}
</div>
<span className="text-sm font-medium text-white">{event.actor}</span>
<span className="text-sm text-zinc-400">{event.description}</span>
</div>
<p className="text-xs text-zinc-600 mt-0.5">{formatRelative(event.timestamp)}</p>
</div>
</div>
);
}
// --- Usage example ---
// const now = new Date();
// const mins = (n: number) => new Date(now.getTime() - n * 60 * 1000);
// const events: AuditEvent[] = [
// { id: 'e1', icon: Rocket, iconColor: '#6366f1', actor: 'Sarah Chen', actorColor: '#6366f1', actorInitials: 'SC', description: 'deployed to production', timestamp: mins(8) },
// { id: 'e2', icon: KeyRound, iconColor: '#f59e0b', actor: 'System', actorColor: '#6b7280', actorInitials: 'SY', description: 'rotated API key', timestamp: mins(45) },
// ];
// <AuditLogTimeline events={events} />Unlock to copy
Free access to all patterns
Command Palette
Linear
Global Search with Results Grouping
Linear
Recent + Suggested Search
Raycast
Filter Builder (AND / OR)
Linear
Sidebar Navigation
Linear
Collapsible Nested Tree Nav
Notion
Tab Bar with Overflow Menu
Vercel
Breadcrumb with Dropdowns
Linear
Workspace Switcher
Slack
Deployment Status Card
Vercel
Empty State
Vercel
Error State with Retry
Notion
Slash Command Menu
Notion
Stats & Metrics Row
Vercel
Single Big Metric Card
Stripe
Usage / Quota Meter
Vercel
Skeleton Loading Grid
Linear
Activity Heatmap
Notion
Confirmation Dialog
Linear
Slide-Over Panel
Stripe
Toast / Snackbar Stack
Vercel
Issue / Task Card
Linear
Activity Feed
Linear + Slack
Comment Thread
Linear
Audit Log Timeline
Vercel
Properties Panel
Notion + Linear
Multi-Step Form Wizard
Stripe
Tag Multi-Select
Linear
Toggle Switch Group
Stripe
Inline Data Table
Notion + Linear
Pagination
Vercel
Sortable / Selectable Table
Linear
Status Badge System
Linear
Segmented Control
Notion