71 / 122 · Observability
Trace waterfall
Every span in a run on one time axis, nested, so you can see where it actually went.
Trace1840ms
Installation
npx shadcn@latest add "@assistant-ui/elements-trace-waterfall"
Usage
import { TraceWaterfall } from "@/components/elements/trace-waterfall";
<TraceWaterfall spans={spans} totalMs={1840} visibleCount={spans.length} />Props
TraceWaterfall
spans*TraceSpan[]Flat list in start order. Nesting comes from each span's depth.
totalMs*numberWidth of the axis. Bars are positioned against this, not against the widest span.
visibleCount*numberHow many spans have been reported so far.
classNamestringExtra classes merged onto the root.
TraceSpan
id*stringStable identity for the row key.
name*stringSpan label in the left rail; truncates rather than wraps.
depth*numberNesting level. Indents the label; the bar position comes from startMs, not from this.
startMs*numberOffset from the start of the trace.
durationMs*numberHow long the span took. A running span still needs one so its bar has width.
status*"running" | "completed" | "failed"Running pulses in blue, completed is neutral, failed is red.
Source
trace-waterfall.tsx"use client";
import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";
export type SpanStatus = "running" | "completed" | "failed";
export interface TraceSpan {
id: string;
name: string;
depth: number;
startMs: number;
durationMs: number;
status: SpanStatus;
}
const TONE: Record<SpanStatus, string> = {
running: "bg-blue-500 dark:bg-blue-400",
completed: "bg-foreground/35",
failed: "bg-red-500/80",
};
export function TraceWaterfall({
spans,
totalMs,
visibleCount,
className,
}: {
spans: readonly TraceSpan[];
totalMs: number;
visibleCount: number;
className?: string;
}) {
const span = totalMs || 1;
return (
<div
className={cn(
paper,
"flex w-full max-w-md flex-col gap-2 rounded-2xl p-4",
className,
)}
>
<div className="flex items-baseline justify-between">
<span className="text-[13.5px] font-medium">Trace</span>
<span className={cn(mono, "text-foreground/35 tabular-nums")}>
{totalMs}ms
</span>
</div>
<div className="flex flex-col gap-1">
{spans.slice(0, visibleCount).map((item) => {
const left = (item.startMs / span) * 100;
const width = Math.max(1.5, (item.durationMs / span) * 100);
return (
<div
key={item.id}
className="fade-in slide-in-from-left-1 animate-in fill-mode-both grid grid-cols-[7.5rem_1fr_2.5rem] items-center gap-2 duration-300"
>
<span
className="text-foreground/70 min-w-0 truncate text-xs"
style={{ paddingInlineStart: `${item.depth * 0.75}rem` }}
>
{item.name}
</span>
<span className="relative flex h-4 min-w-0 items-center">
<span
className={cn(
"absolute h-[7px] rounded-full transition-[width] duration-500 ease-out motion-reduce:transition-none",
TONE[item.status],
item.status === "running" && "animate-pulse",
)}
style={{ insetInlineStart: `${left}%`, width: `${width}%` }}
/>
</span>
<span
className={cn(mono, "text-foreground/30 text-end tabular-nums")}
>
{item.durationMs}
</span>
</div>
);
})}
</div>
</div>
);
}