Log Stream
MarketingA live log/event viewer that stays smooth at tens of thousands of rows and tails output like a terminal. Hand-written fixed-height virtualization renders only the slice around the viewport, offset with one translateY over a full-height spacer, from a rAF-throttled scroll handler. It sticks to the bottom while you're pinned to the tail and lets go the moment you scroll up — counting unseen lines and offering a jump-to-latest pill. Filter, pause, and clear included; monochrome.
1280pxOpen
components/blocks/log-stream.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
/**
* LogStream — a live log/event viewer that stays smooth at tens of thousands of
* rows and tails new output like a terminal.
*
* Two hard parts, both hand-written (no virtualization or autoscroll library):
*
* 1. **Fixed-height virtualization.** The scroller holds one tall spacer sized
* to `rows × ROW_H`; only the slice around the viewport is rendered, offset
* into place with a single `translateY`. The visible range is derived from
* `scrollTop` in a rAF-throttled handler, and `start` only re-renders when
* the slice actually changes.
* 2. **Stick to bottom unless scrolled up.** A `stick` ref tracks whether you're
* pinned to the tail (within a few px of the bottom). New rows scroll to the
* bottom *only* while pinned; scroll up and it lets go, counting unseen lines
* and offering a "jump to latest" pill. `unseen` is derived (`total − seen`),
* so nothing sets state inside the append effect.
*
* Monochrome; levels read by weight, not colour.
*/
type Level = "DEBUG" | "INFO" | "WARN" | "ERROR";
interface Line {
id: number;
t: string;
level: Level;
msg: string;
}
const ROW_H = 24; // px — every row is exactly this tall
const OVERSCAN = 8; // rows rendered beyond the viewport, each side
const SEED = 800;
const SOURCES = ["api", "worker", "db", "cache", "auth", "edge", "cron"];
const MESSAGES = [
"request completed",
"cache miss — falling back to origin",
"connection pool at 82% capacity",
"retrying upstream after 503",
"token refreshed",
"slow query 1.2s over threshold",
"job enqueued",
"rate limit near ceiling for tenant",
"healthcheck ok",
"dropped stale websocket",
];
const LEVELS: Level[] = ["DEBUG", "INFO", "INFO", "INFO", "WARN", "ERROR"];
function makeLine(id: number): Line {
const level = LEVELS[Math.floor(Math.random() * LEVELS.length)];
const src = SOURCES[id % SOURCES.length];
const msg = MESSAGES[Math.floor(Math.random() * MESSAGES.length)];
const secs = 39_000 + id; // fake monotonic clock
const hh = String(Math.floor(secs / 3600) % 24).padStart(2, "0");
const mm = String(Math.floor(secs / 60) % 60).padStart(2, "0");
const ss = String(secs % 60).padStart(2, "0");
return { id, t: `${hh}:${mm}:${ss}`, level, msg: `[${src}] ${msg}` };
}
const seed = (): Line[] => Array.from({ length: SEED }, (_, i) => makeLine(i));
const LEVEL_CLASS: Record<Level, string> = {
DEBUG: "text-zinc-500/60 dark:text-zinc-400/60",
INFO: "text-zinc-500 dark:text-zinc-400",
WARN: "text-zinc-950 dark:text-zinc-50",
ERROR: "font-semibold text-zinc-950 dark:text-zinc-50",
};
type Filter = "all" | "warn" | "error";
const FILTERS: { id: Filter; label: string }[] = [
{ id: "all", label: "All" },
{ id: "warn", label: "Warn+" },
{ id: "error", label: "Errors" },
];
const RANK: Record<Level, number> = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
export function LogStream() {
const scrollerRef = React.useRef<HTMLDivElement>(null);
const stick = React.useRef(true);
const raf = React.useRef(0);
const nextId = React.useRef(SEED);
const [lines, setLines] = React.useState<Line[]>(seed);
const [filter, setFilter] = React.useState<Filter>("all");
const [paused, setPaused] = React.useState(false);
const [start, setStart] = React.useState(0);
const [viewH, setViewH] = React.useState(320);
const [seen, setSeen] = React.useState(SEED);
const displayed = React.useMemo(() => {
if (filter === "all") return lines;
const min = filter === "warn" ? RANK.WARN : RANK.ERROR;
return lines.filter((l) => RANK[l.level] >= min);
}, [lines, filter]);
const total = displayed.length;
const unseen = Math.max(0, total - seen);
const count = Math.ceil(viewH / ROW_H) + OVERSCAN * 2;
const end = Math.min(total, start + count);
const visible = displayed.slice(start, end);
const scrollToBottom = React.useCallback(() => {
const el = scrollerRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, []);
// Measure the viewport (subscription — setState in the callback, not the body).
React.useEffect(() => {
const el = scrollerRef.current;
if (!el) return;
const ro = new ResizeObserver(([e]) => setViewH(e.contentRect.height));
ro.observe(el);
return () => ro.disconnect();
}, []);
const onScroll = () => {
if (raf.current) return;
raf.current = requestAnimationFrame(() => {
raf.current = 0;
const el = scrollerRef.current;
if (!el) return;
const top = el.scrollTop;
setStart(Math.max(0, Math.floor(top / ROW_H) - OVERSCAN));
const atBottom = el.scrollHeight - el.clientHeight - top < 4;
stick.current = atBottom;
if (atBottom) setSeen(total);
});
};
// After new rows land, keep the tail pinned if we were following it.
React.useLayoutEffect(() => {
if (stick.current) scrollToBottom();
}, [total, scrollToBottom]);
// Simulated stream.
React.useEffect(() => {
if (paused) return;
const id = window.setInterval(() => {
const n = 1 + Math.floor(Math.random() * 3);
setLines((prev) => [
...prev,
...Array.from({ length: n }, () => makeLine(nextId.current++)),
]);
}, 600);
return () => window.clearInterval(id);
}, [paused]);
const jumpToLatest = () => {
stick.current = true;
setSeen(total);
scrollToBottom();
};
const clear = () => {
setLines([]);
setSeen(0);
nextId.current = 0;
stick.current = true;
};
return (
<section className="bg-zinc-50 px-6 py-16 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
<div className="mx-auto max-w-2xl">
<div className="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
{/* Toolbar */}
<div className="flex items-center gap-3 border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
<span className="flex items-center gap-2 font-mono text-xs text-zinc-500 dark:text-zinc-400">
<span
data-live={!paused}
className="dark:true]:bg-zinc-50 size-1.5 rounded-full bg-zinc-500 data-[live=true]:bg-zinc-950 data-[live=true]:motion-safe:animate-pulse dark:bg-zinc-400"
/>
{total.toLocaleString()} events
</span>
<div className="ml-auto inline-flex rounded-md border border-zinc-200 bg-zinc-50 p-0.5 dark:border-zinc-800 dark:bg-zinc-950">
{FILTERS.map((f) => (
<button
key={f.id}
type="button"
aria-pressed={filter === f.id}
onClick={() => setFilter(f.id)}
className={cn(
"rounded px-2 py-1 text-xs transition-colors",
filter === f.id
? "bg-zinc-950 text-zinc-50 dark:bg-zinc-50 dark:text-zinc-950"
: "text-zinc-500 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-zinc-50",
)}
>
{f.label}
</button>
))}
</div>
<button
type="button"
onClick={() => setPaused((p) => !p)}
className="rounded-md border border-zinc-200 px-2 py-1 text-xs text-zinc-500 transition-colors hover:text-zinc-950 dark:border-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-50"
>
{paused ? "Resume" : "Pause"}
</button>
<button
type="button"
onClick={clear}
className="rounded-md px-2 py-1 text-xs text-zinc-500 transition-colors hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-zinc-50"
>
Clear
</button>
</div>
{/* Virtualized viewport */}
<div className="relative">
<div
ref={scrollerRef}
onScroll={onScroll}
role="log"
aria-label="Event stream"
tabIndex={0}
className="h-80 overflow-y-auto overscroll-contain focus-visible:ring-2 focus-visible:ring-zinc-950/40 focus-visible:outline-none focus-visible:ring-inset dark:focus-visible:ring-zinc-50/40"
>
{total === 0 ? (
<div className="grid h-full place-items-center text-sm text-zinc-500 dark:text-zinc-400">
No events.
</div>
) : (
<div style={{ height: total * ROW_H }}>
<div style={{ transform: `translateY(${start * ROW_H}px)` }}>
{visible.map((line) => (
<div
key={line.id}
style={{ height: ROW_H }}
className="flex items-center gap-3 px-3 font-mono text-xs whitespace-nowrap"
>
<span className="shrink-0 text-zinc-500/70 tabular-nums dark:text-zinc-400/70">
{line.t}
</span>
<span
className={cn(
"w-12 shrink-0 tabular-nums",
LEVEL_CLASS[line.level],
)}
>
{line.level}
</span>
<span className="truncate text-zinc-950/90 dark:text-zinc-50/90">
{line.msg}
</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Jump-to-latest pill */}
{unseen > 0 ? (
<button
type="button"
onClick={jumpToLatest}
className="absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-zinc-200 bg-white px-3 py-1.5 text-xs font-medium text-zinc-950 shadow-lg transition-colors hover:bg-zinc-100 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-50 dark:hover:bg-zinc-800"
>
{unseen.toLocaleString()} new
<svg
viewBox="0 0 24 24"
className="size-3.5"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 5v14M19 12l-7 7-7-7" />
</svg>
</button>
) : null}
</div>
</div>
</div>
</section>
);
}Installation
Terminal
npx shadcn@latest add https://ui.saumyarex.xyz/r/log-stream.jsonInstalls the block and its component dependencies in one step.
Install dependencies
Terminal
npm install clsx tailwind-mergeCopy the source
components/blocks/log-stream.tsx
"use client";
import * as React from "react";
import { cn } from "@/lib/utils";
/**
* LogStream — a live log/event viewer that stays smooth at tens of thousands of
* rows and tails new output like a terminal.
*
* Two hard parts, both hand-written (no virtualization or autoscroll library):
*
* 1. **Fixed-height virtualization.** The scroller holds one tall spacer sized
* to `rows × ROW_H`; only the slice around the viewport is rendered, offset
* into place with a single `translateY`. The visible range is derived from
* `scrollTop` in a rAF-throttled handler, and `start` only re-renders when
* the slice actually changes.
* 2. **Stick to bottom unless scrolled up.** A `stick` ref tracks whether you're
* pinned to the tail (within a few px of the bottom). New rows scroll to the
* bottom *only* while pinned; scroll up and it lets go, counting unseen lines
* and offering a "jump to latest" pill. `unseen` is derived (`total − seen`),
* so nothing sets state inside the append effect.
*
* Monochrome; levels read by weight, not colour.
*/
type Level = "DEBUG" | "INFO" | "WARN" | "ERROR";
interface Line {
id: number;
t: string;
level: Level;
msg: string;
}
const ROW_H = 24; // px — every row is exactly this tall
const OVERSCAN = 8; // rows rendered beyond the viewport, each side
const SEED = 800;
const SOURCES = ["api", "worker", "db", "cache", "auth", "edge", "cron"];
const MESSAGES = [
"request completed",
"cache miss — falling back to origin",
"connection pool at 82% capacity",
"retrying upstream after 503",
"token refreshed",
"slow query 1.2s over threshold",
"job enqueued",
"rate limit near ceiling for tenant",
"healthcheck ok",
"dropped stale websocket",
];
const LEVELS: Level[] = ["DEBUG", "INFO", "INFO", "INFO", "WARN", "ERROR"];
function makeLine(id: number): Line {
const level = LEVELS[Math.floor(Math.random() * LEVELS.length)];
const src = SOURCES[id % SOURCES.length];
const msg = MESSAGES[Math.floor(Math.random() * MESSAGES.length)];
const secs = 39_000 + id; // fake monotonic clock
const hh = String(Math.floor(secs / 3600) % 24).padStart(2, "0");
const mm = String(Math.floor(secs / 60) % 60).padStart(2, "0");
const ss = String(secs % 60).padStart(2, "0");
return { id, t: `${hh}:${mm}:${ss}`, level, msg: `[${src}] ${msg}` };
}
const seed = (): Line[] => Array.from({ length: SEED }, (_, i) => makeLine(i));
const LEVEL_CLASS: Record<Level, string> = {
DEBUG: "text-zinc-500/60 dark:text-zinc-400/60",
INFO: "text-zinc-500 dark:text-zinc-400",
WARN: "text-zinc-950 dark:text-zinc-50",
ERROR: "font-semibold text-zinc-950 dark:text-zinc-50",
};
type Filter = "all" | "warn" | "error";
const FILTERS: { id: Filter; label: string }[] = [
{ id: "all", label: "All" },
{ id: "warn", label: "Warn+" },
{ id: "error", label: "Errors" },
];
const RANK: Record<Level, number> = { DEBUG: 0, INFO: 1, WARN: 2, ERROR: 3 };
export function LogStream() {
const scrollerRef = React.useRef<HTMLDivElement>(null);
const stick = React.useRef(true);
const raf = React.useRef(0);
const nextId = React.useRef(SEED);
const [lines, setLines] = React.useState<Line[]>(seed);
const [filter, setFilter] = React.useState<Filter>("all");
const [paused, setPaused] = React.useState(false);
const [start, setStart] = React.useState(0);
const [viewH, setViewH] = React.useState(320);
const [seen, setSeen] = React.useState(SEED);
const displayed = React.useMemo(() => {
if (filter === "all") return lines;
const min = filter === "warn" ? RANK.WARN : RANK.ERROR;
return lines.filter((l) => RANK[l.level] >= min);
}, [lines, filter]);
const total = displayed.length;
const unseen = Math.max(0, total - seen);
const count = Math.ceil(viewH / ROW_H) + OVERSCAN * 2;
const end = Math.min(total, start + count);
const visible = displayed.slice(start, end);
const scrollToBottom = React.useCallback(() => {
const el = scrollerRef.current;
if (el) el.scrollTop = el.scrollHeight;
}, []);
// Measure the viewport (subscription — setState in the callback, not the body).
React.useEffect(() => {
const el = scrollerRef.current;
if (!el) return;
const ro = new ResizeObserver(([e]) => setViewH(e.contentRect.height));
ro.observe(el);
return () => ro.disconnect();
}, []);
const onScroll = () => {
if (raf.current) return;
raf.current = requestAnimationFrame(() => {
raf.current = 0;
const el = scrollerRef.current;
if (!el) return;
const top = el.scrollTop;
setStart(Math.max(0, Math.floor(top / ROW_H) - OVERSCAN));
const atBottom = el.scrollHeight - el.clientHeight - top < 4;
stick.current = atBottom;
if (atBottom) setSeen(total);
});
};
// After new rows land, keep the tail pinned if we were following it.
React.useLayoutEffect(() => {
if (stick.current) scrollToBottom();
}, [total, scrollToBottom]);
// Simulated stream.
React.useEffect(() => {
if (paused) return;
const id = window.setInterval(() => {
const n = 1 + Math.floor(Math.random() * 3);
setLines((prev) => [
...prev,
...Array.from({ length: n }, () => makeLine(nextId.current++)),
]);
}, 600);
return () => window.clearInterval(id);
}, [paused]);
const jumpToLatest = () => {
stick.current = true;
setSeen(total);
scrollToBottom();
};
const clear = () => {
setLines([]);
setSeen(0);
nextId.current = 0;
stick.current = true;
};
return (
<section className="bg-zinc-50 px-6 py-16 text-zinc-950 dark:bg-zinc-950 dark:text-zinc-50">
<div className="mx-auto max-w-2xl">
<div className="overflow-hidden rounded-2xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900">
{/* Toolbar */}
<div className="flex items-center gap-3 border-b border-zinc-200 px-3 py-2 dark:border-zinc-800">
<span className="flex items-center gap-2 font-mono text-xs text-zinc-500 dark:text-zinc-400">
<span
data-live={!paused}
className="dark:true]:bg-zinc-50 size-1.5 rounded-full bg-zinc-500 data-[live=true]:bg-zinc-950 data-[live=true]:motion-safe:animate-pulse dark:bg-zinc-400"
/>
{total.toLocaleString()} events
</span>
<div className="ml-auto inline-flex rounded-md border border-zinc-200 bg-zinc-50 p-0.5 dark:border-zinc-800 dark:bg-zinc-950">
{FILTERS.map((f) => (
<button
key={f.id}
type="button"
aria-pressed={filter === f.id}
onClick={() => setFilter(f.id)}
className={cn(
"rounded px-2 py-1 text-xs transition-colors",
filter === f.id
? "bg-zinc-950 text-zinc-50 dark:bg-zinc-50 dark:text-zinc-950"
: "text-zinc-500 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-zinc-50",
)}
>
{f.label}
</button>
))}
</div>
<button
type="button"
onClick={() => setPaused((p) => !p)}
className="rounded-md border border-zinc-200 px-2 py-1 text-xs text-zinc-500 transition-colors hover:text-zinc-950 dark:border-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-50"
>
{paused ? "Resume" : "Pause"}
</button>
<button
type="button"
onClick={clear}
className="rounded-md px-2 py-1 text-xs text-zinc-500 transition-colors hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-zinc-50"
>
Clear
</button>
</div>
{/* Virtualized viewport */}
<div className="relative">
<div
ref={scrollerRef}
onScroll={onScroll}
role="log"
aria-label="Event stream"
tabIndex={0}
className="h-80 overflow-y-auto overscroll-contain focus-visible:ring-2 focus-visible:ring-zinc-950/40 focus-visible:outline-none focus-visible:ring-inset dark:focus-visible:ring-zinc-50/40"
>
{total === 0 ? (
<div className="grid h-full place-items-center text-sm text-zinc-500 dark:text-zinc-400">
No events.
</div>
) : (
<div style={{ height: total * ROW_H }}>
<div style={{ transform: `translateY(${start * ROW_H}px)` }}>
{visible.map((line) => (
<div
key={line.id}
style={{ height: ROW_H }}
className="flex items-center gap-3 px-3 font-mono text-xs whitespace-nowrap"
>
<span className="shrink-0 text-zinc-500/70 tabular-nums dark:text-zinc-400/70">
{line.t}
</span>
<span
className={cn(
"w-12 shrink-0 tabular-nums",
LEVEL_CLASS[line.level],
)}
>
{line.level}
</span>
<span className="truncate text-zinc-950/90 dark:text-zinc-50/90">
{line.msg}
</span>
</div>
))}
</div>
</div>
)}
</div>
{/* Jump-to-latest pill */}
{unseen > 0 ? (
<button
type="button"
onClick={jumpToLatest}
className="absolute bottom-3 left-1/2 flex -translate-x-1/2 items-center gap-1.5 rounded-full border border-zinc-200 bg-white px-3 py-1.5 text-xs font-medium text-zinc-950 shadow-lg transition-colors hover:bg-zinc-100 dark:border-zinc-800 dark:bg-zinc-900 dark:text-zinc-50 dark:hover:bg-zinc-800"
>
{unseen.toLocaleString()} new
<svg
viewBox="0 0 24 24"
className="size-3.5"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 5v14M19 12l-7 7-7-7" />
</svg>
</button>
) : null}
</div>
</div>
</div>
</section>
);
}lib/utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/** Merge conditional class names and resolve Tailwind conflicts. */
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
/** Shared view-transition name so a card preview morphs into the detail
* page's preview. Must match on both ends; unique per registry entry. */
export function previewTransitionName(name: string) {
return `preview-${name}`;
}