Compact Timeline
Original · freecompact log timeline
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/tl-compact.json"use client";
import { useId, useMemo, useState } from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
type Level = "success" | "info" | "warn" | "error";
interface LogEvent {
id: string;
time: string;
stamp: string;
level: Level;
source: string;
title: string;
detail: string;
duration: string;
}
const EVENTS: LogEvent[] = [
{
id: "evt-4821",
time: "09:42:07",
stamp: "2026-07-16 09:42:07 UTC",
level: "success",
source: "api-gateway",
title: "Deployment v2.4.1 promoted to production",
detail:
"Rolling update completed across 24 pods in 3 regions. All readiness probes returned healthy and traffic shifted at 100% with zero failed requests.",
duration: "1m 12s",
},
{
id: "evt-4820",
time: "09:41:52",
stamp: "2026-07-16 09:41:52 UTC",
level: "info",
source: "ci-runner",
title: "Build #4821 finished — 1,204 modules compiled",
detail:
"Compiled in 38.2s with a 91% cache hit ratio. Bundle size increased 4.1 KB gzip versus the previous green build on main.",
duration: "38.2s",
},
{
id: "evt-4819",
time: "09:40:18",
stamp: "2026-07-16 09:40:18 UTC",
level: "warn",
source: "billing-worker",
title: "Retry queue depth above threshold (412 / 400)",
detail:
"Pending jobs crossed the configured ceiling for 90 seconds. Horizontal autoscaler added 2 workers; queue expected to drain within 4 minutes.",
duration: "90s",
},
{
id: "evt-4818",
time: "09:38:44",
stamp: "2026-07-16 09:38:44 UTC",
level: "error",
source: "auth-service",
title: "Token refresh failed for tenant acme-co",
detail:
"Upstream identity provider returned HTTP 503 five times in a row. The circuit breaker opened and requests are shedding to the cached-grant fallback.",
duration: "5 attempts",
},
{
id: "evt-4817",
time: "09:37:10",
stamp: "2026-07-16 09:37:10 UTC",
level: "success",
source: "db-migrator",
title: "Migration 0142_add_audit_index applied",
detail:
"Created a concurrent B-tree index on events.created_at without locking writes. Table now holds 41.2M rows; planner picked the new index on first query.",
duration: "6.4s",
},
{
id: "evt-4816",
time: "09:35:29",
stamp: "2026-07-16 09:35:29 UTC",
level: "info",
source: "edge-cache",
title: "Purged CDN cache for /assets/*",
detail:
"Invalidated 8,431 objects across 210 edge locations. Estimated global propagation is roughly 30 seconds; origin shielding absorbed the refill traffic.",
duration: "28s",
},
{
id: "evt-4815",
time: "09:33:55",
stamp: "2026-07-16 09:33:55 UTC",
level: "warn",
source: "search-indexer",
title: "Reindex lag detected — 4m 12s behind primary",
detail:
"Backfill is running at 3.1k docs/sec. Reads are served from the last consistent snapshot, so recent writes may not appear in results until it catches up.",
duration: "4m 12s",
},
{
id: "evt-4814",
time: "09:31:02",
stamp: "2026-07-16 09:31:02 UTC",
level: "success",
source: "webhook-dispatcher",
title: "Delivered 2,048 events within SLA",
detail:
"Every subscriber acknowledged inside the 500ms window. P99 delivery latency was 214ms; no payloads were routed to the dead-letter queue.",
duration: "3.7s",
},
{
id: "evt-4813",
time: "09:29:47",
stamp: "2026-07-16 09:29:47 UTC",
level: "error",
source: "image-resizer",
title: "Job rejected — unsupported color profile",
detail:
"File user-9921/banner.tiff uses a CMYK profile the pipeline cannot rasterize. The upload was quarantined and the owner was notified to re-export as sRGB.",
duration: "—",
},
{
id: "evt-4812",
time: "09:27:33",
stamp: "2026-07-16 09:27:33 UTC",
level: "info",
source: "scheduler",
title: "Nightly backup enqueued for 3 volumes",
detail:
"Encrypted snapshots are scheduled for 02:00 UTC with a 30-day retention window. Prior run restored cleanly during last week's recovery drill.",
duration: "queued",
},
];
const LEVEL_STYLES: Record<
Level,
{ label: string; dot: string; source: string; chipOn: string }
> = {
success: {
label: "Success",
dot: "bg-emerald-500 ring-emerald-500/25 dark:bg-emerald-400 dark:ring-emerald-400/25",
source: "text-emerald-700 dark:text-emerald-300",
chipOn:
"border-emerald-400 bg-emerald-50 text-emerald-700 dark:border-emerald-400/50 dark:bg-emerald-400/10 dark:text-emerald-300",
},
info: {
label: "Info",
dot: "bg-sky-500 ring-sky-500/25 dark:bg-sky-400 dark:ring-sky-400/25",
source: "text-sky-700 dark:text-sky-300",
chipOn:
"border-sky-400 bg-sky-50 text-sky-700 dark:border-sky-400/50 dark:bg-sky-400/10 dark:text-sky-300",
},
warn: {
label: "Warning",
dot: "bg-amber-500 ring-amber-500/25 dark:bg-amber-400 dark:ring-amber-400/25",
source: "text-amber-700 dark:text-amber-300",
chipOn:
"border-amber-400 bg-amber-50 text-amber-700 dark:border-amber-400/50 dark:bg-amber-400/10 dark:text-amber-300",
},
error: {
label: "Error",
dot: "bg-rose-500 ring-rose-500/25 dark:bg-rose-400 dark:ring-rose-400/25",
source: "text-rose-700 dark:text-rose-300",
chipOn:
"border-rose-400 bg-rose-50 text-rose-700 dark:border-rose-400/50 dark:bg-rose-400/10 dark:text-rose-300",
},
};
const LEVEL_ORDER: Level[] = ["success", "info", "warn", "error"];
function LevelIcon({ level, className }: { level: Level; className?: string }) {
const common = {
className,
viewBox: "0 0 20 20",
fill: "none",
stroke: "currentColor",
strokeWidth: 1.75,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
"aria-hidden": true,
};
if (level === "success") {
return (
<svg {...common}>
<circle cx="10" cy="10" r="7.25" />
<path d="m6.75 10 2.25 2.25 4.25-4.5" />
</svg>
);
}
if (level === "info") {
return (
<svg {...common}>
<circle cx="10" cy="10" r="7.25" />
<path d="M10 9.25v4" />
<path d="M10 6.75h.01" />
</svg>
);
}
if (level === "warn") {
return (
<svg {...common}>
<path d="M10 3.5 2.75 16h14.5L10 3.5Z" />
<path d="M10 8.5v3.25" />
<path d="M10 14h.01" />
</svg>
);
}
return (
<svg {...common}>
<circle cx="10" cy="10" r="7.25" />
<path d="m7.75 7.75 4.5 4.5" />
<path d="m12.25 7.75-4.5 4.5" />
</svg>
);
}
function SearchIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<circle cx="9" cy="9" r="5.25" />
<path d="m13 13 3.5 3.5" />
</svg>
);
}
function ChevronIcon({ className }: { className?: string }) {
return (
<svg
className={className}
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<path d="m6 8 4 4 4-4" />
</svg>
);
}
export default function TlCompact() {
const uid = useId();
const reduce = useReducedMotion();
const [active, setActive] = useState<Set<Level>>(
() => new Set<Level>(LEVEL_ORDER),
);
const [query, setQuery] = useState("");
const [openId, setOpenId] = useState<string | null>(EVENTS[0].id);
const allOn = active.size === LEVEL_ORDER.length;
const ring =
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950";
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
return EVENTS.filter((e) => {
if (!active.has(e.level)) return false;
if (!q) return true;
return (
e.title.toLowerCase().includes(q) ||
e.source.toLowerCase().includes(q) ||
e.detail.toLowerCase().includes(q) ||
LEVEL_STYLES[e.level].label.toLowerCase().includes(q)
);
});
}, [active, query]);
function toggleLevel(level: Level) {
setActive((prev) => {
const next = new Set(prev);
if (next.has(level)) next.delete(level);
else next.add(level);
return next;
});
}
return (
<section className="relative w-full bg-slate-50 px-4 py-16 text-slate-900 sm:px-6 sm:py-20 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes tlcompact-ping {
0% { transform: scale(1); opacity: 0.6; }
70%, 100% { transform: scale(2.6); opacity: 0; }
}
.tlcompact-ping { animation: tlcompact-ping 2.4s cubic-bezier(0,0,0.2,1) infinite; }
@media (prefers-reduced-motion: reduce) {
.tlcompact-ping { animation: none; }
}
`}</style>
<div className="mx-auto max-w-2xl">
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
{/* Header */}
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-4 py-4 sm:px-5 dark:border-slate-800">
<div className="flex items-center gap-3">
<span className="relative flex h-2.5 w-2.5" aria-hidden>
<span className="tlcompact-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-emerald-500" />
</span>
<div>
<h2 className="text-sm font-semibold tracking-tight text-slate-900 dark:text-white">
Activity log
</h2>
<p className="text-xs text-slate-500 dark:text-slate-400">
Live · orion-prod cluster
</p>
</div>
</div>
<span className="rounded-full border border-slate-200 bg-slate-50 px-2.5 py-1 font-mono text-xs tabular-nums text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
{filtered.length} / {EVENTS.length}
</span>
</div>
{/* Toolbar */}
<div className="space-y-3 border-b border-slate-200 px-4 py-3 sm:px-5 dark:border-slate-800">
<div className="relative">
<SearchIcon className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-slate-400 dark:text-slate-500" />
<label htmlFor={`${uid}-search`} className="sr-only">
Filter events by text
</label>
<input
id={`${uid}-search`}
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Filter by service, message, level…"
className={`w-full rounded-lg border border-slate-200 bg-slate-50 py-2 pl-9 pr-8 text-sm text-slate-800 placeholder:text-slate-400 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100 dark:placeholder:text-slate-500 ${ring}`}
/>
{query && (
<button
type="button"
onClick={() => setQuery("")}
aria-label="Clear filter text"
className={`absolute right-2 top-1/2 flex h-5 w-5 -translate-y-1/2 items-center justify-center rounded text-slate-400 hover:text-slate-700 dark:hover:text-slate-200 ${ring}`}
>
<svg
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth={1.75}
strokeLinecap="round"
className="h-3.5 w-3.5"
aria-hidden
>
<path d="m5.5 5.5 9 9M14.5 5.5l-9 9" />
</svg>
</button>
)}
</div>
<div className="flex flex-wrap items-center gap-2">
<button
type="button"
aria-pressed={allOn}
onClick={() => setActive(new Set(LEVEL_ORDER))}
className={`rounded-full border px-3 py-1 text-xs font-medium transition-colors motion-reduce:transition-none ${ring} ${
allOn
? "border-indigo-400 bg-indigo-50 text-indigo-700 dark:border-indigo-400/50 dark:bg-indigo-400/10 dark:text-indigo-300"
: "border-slate-200 bg-white text-slate-600 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
}`}
>
All
</button>
{LEVEL_ORDER.map((level) => {
const on = active.has(level);
const s = LEVEL_STYLES[level];
return (
<button
key={level}
type="button"
aria-pressed={on}
onClick={() => toggleLevel(level)}
className={`inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-medium transition-colors motion-reduce:transition-none ${ring} ${
on
? s.chipOn
: "border-slate-200 bg-white text-slate-500 hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-400 dark:hover:bg-slate-700"
}`}
>
<span
className={`h-1.5 w-1.5 rounded-full ${
on ? s.dot.split(" ")[0] : "bg-slate-300 dark:bg-slate-600"
}`}
aria-hidden
/>
{s.label}
</button>
);
})}
</div>
</div>
{/* Log list */}
<ul className="relative">
<AnimatePresence initial={false}>
{filtered.map((e, i) => {
const s = LEVEL_STYLES[e.level];
const open = openId === e.id;
const panelId = `${uid}-panel-${e.id}`;
const btnId = `${uid}-btn-${e.id}`;
const isFirst = i === 0;
const isLast = i === filtered.length - 1;
return (
<motion.li
key={e.id}
initial={reduce ? false : { opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
exit={reduce ? { opacity: 0 } : { opacity: 0, y: -6 }}
transition={{ duration: 0.2 }}
className="border-t border-slate-100 first:border-t-0 dark:border-slate-800/70"
>
<button
type="button"
id={btnId}
onClick={() => setOpenId(open ? null : e.id)}
aria-expanded={open}
aria-controls={panelId}
className={`group grid w-full grid-cols-[3.25rem_1.25rem_minmax(0,1fr)_1.25rem] items-start gap-x-3 px-4 py-3 text-left transition-colors hover:bg-slate-50 sm:px-5 dark:hover:bg-slate-800/50 ${ring}`}
>
<span className="pt-0.5 font-mono text-xs tabular-nums text-slate-400 dark:text-slate-500">
{e.time}
</span>
<span className="relative flex justify-center self-stretch">
<span
aria-hidden
className={`absolute left-1/2 w-px -translate-x-1/2 bg-slate-200 dark:bg-slate-700/70 ${
isFirst ? "top-2" : "top-0"
} ${isLast ? "bottom-2" : "bottom-0"}`}
/>
<span
className={`relative z-10 mt-1.5 h-2.5 w-2.5 rounded-full ring-4 ${s.dot}`}
aria-hidden
/>
</span>
<span className="min-w-0">
<span className="flex items-baseline gap-2">
<span className="sr-only">{s.label}: </span>
<span
className={`shrink-0 font-mono text-[11px] font-medium ${s.source}`}
>
{e.source}
</span>
<span className="truncate text-sm text-slate-700 dark:text-slate-200">
{e.title}
</span>
</span>
</span>
<ChevronIcon
className={`mt-1 h-4 w-4 text-slate-400 transition-transform duration-200 motion-reduce:transition-none dark:text-slate-500 ${
open ? "rotate-180" : ""
}`}
/>
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div
key="panel"
id={panelId}
role="region"
aria-labelledby={btnId}
initial={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
animate={
reduce ? { opacity: 1 } : { height: "auto", opacity: 1 }
}
exit={reduce ? { opacity: 0 } : { height: 0, opacity: 0 }}
transition={{ duration: 0.22, ease: [0.4, 0, 0.2, 1] }}
className="overflow-hidden"
>
<div className="px-4 pb-4 pl-14 sm:px-5 sm:pl-[4.5rem]">
<div className="rounded-lg border border-slate-100 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/40">
<div className="mb-2 flex items-center gap-1.5">
<LevelIcon
level={e.level}
className={`h-4 w-4 ${s.source}`}
/>
<span
className={`text-[11px] font-semibold uppercase tracking-wide ${s.source}`}
>
{s.label}
</span>
</div>
<p className="text-sm leading-relaxed text-slate-600 dark:text-slate-300">
{e.detail}
</p>
<dl className="mt-3 flex flex-wrap gap-x-6 gap-y-1.5 border-t border-slate-200 pt-3 text-xs dark:border-slate-700">
<div className="flex gap-1.5">
<dt className="text-slate-400 dark:text-slate-500">
Event
</dt>
<dd className="font-mono text-slate-600 dark:text-slate-300">
{e.id}
</dd>
</div>
<div className="flex gap-1.5">
<dt className="text-slate-400 dark:text-slate-500">
When
</dt>
<dd className="font-mono tabular-nums text-slate-600 dark:text-slate-300">
{e.stamp}
</dd>
</div>
<div className="flex gap-1.5">
<dt className="text-slate-400 dark:text-slate-500">
Duration
</dt>
<dd className="font-mono text-slate-600 dark:text-slate-300">
{e.duration}
</dd>
</div>
</dl>
</div>
</div>
</motion.div>
)}
</AnimatePresence>
</motion.li>
);
})}
</AnimatePresence>
{filtered.length === 0 && (
<li className="px-5 py-12 text-center">
<p className="text-sm font-medium text-slate-600 dark:text-slate-300">
No events match your filters
</p>
<p className="mt-1 text-xs text-slate-400 dark:text-slate-500">
Try enabling more levels or clearing the search.
</p>
</li>
)}
</ul>
{/* Footer */}
<div className="flex items-center justify-between border-t border-slate-200 px-4 py-2.5 sm:px-5 dark:border-slate-800">
<p className="text-xs text-slate-400 dark:text-slate-500">
Retained 30 days · UTC
</p>
<p aria-live="polite" className="text-xs text-slate-500 dark:text-slate-400">
Showing{" "}
<span className="font-mono tabular-nums text-slate-700 dark:text-slate-200">
{filtered.length}
</span>{" "}
of {EVENTS.length} events
</p>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Vertical Timeline
Originalvertical timeline with dots

Centered Timeline
Originalcentred alternating timeline
Icons Timeline
Originaltimeline with icon nodes

Cards Timeline
Originaltimeline with card entries

Horizontal Timeline
Originalhorizontal timeline

Gradient Timeline
Originaltimeline with a gradient line

Activity Timeline
Originalactivity feed timeline

Roadmap Timeline
Originalproduct roadmap timeline

Numbered Timeline
Originalnumbered milestone timeline

Spotlight Hero
OriginalA centred hero with a soft radial spotlight, badge and dual call-to-action.

Split Hero
OriginalA two-column hero pairing a headline and CTAs with a product mock and social proof.

Gradient Spotlight Hero
OriginalA minimal centred hero with a soft gradient-mesh backdrop, announcement pill and dual call-to-action buttons.

