Timeline
The spans of a trace on one axis of time: a tree of rows that folds, a bar per span colored by what it waits on, the time each kind takes, moments such as errors on the axis, and the details of the selected span under its row. An order request with its queries, a service that calls a tax API, and a payment check that times out:
import { Timeline, type TimelineKind, type TimelineSpan } from '@adecore/charts';
import { formatPercent, formatPreciseDuration } from '@adecore/ui/format';
interface TraceSpan extends TimelineSpan {
readonly op: string;
readonly ok: boolean;
}
const START = Date.UTC(2026, 9, 24, 14, 12, 3, 120);
const span = (id: string, parent: string | null, op: string, label: string, offset: number, duration: number, ok = true): TraceSpan => ({
id,
parent,
op,
label,
start: START + offset,
duration,
kind: op.startsWith('db') ? 'db' : op.startsWith('http') ? 'http' : 'code',
tone: ok ? undefined : 'error',
ok
});
/* An order page: two queries up front, the price worked out by a service that calls the tax API, and a payment check that times out. */
const SPANS: TraceSpan[] = [
span('a1', null, 'http.server', 'GET /api/orders/1842', 0, 412),
span('b1', 'a1', 'middleware', 'Authenticate', 2, 18),
span('b2', 'b1', 'db.query', 'SELECT * FROM sessions WHERE token = ?', 4, 9),
span('b3', 'a1', 'db.query', 'SELECT * FROM orders WHERE id = ?', 22, 31),
span('b4', 'a1', 'db.query', 'SELECT * FROM order_lines WHERE order_id = ?', 55, 14),
span('c1', 'a1', 'function', 'OrderService.price', 72, 168),
span('c2', 'c1', 'db.query', 'SELECT * FROM discounts WHERE code IN (?)', 75, 6.4),
span('c3', 'c1', 'http.client', 'POST tax.example.com/v2/quote', 84, 141),
span('c4', 'c1', 'function', 'Money.round', 228, 0.8),
span('d1', 'a1', 'http.client', 'GET payments.example.com/status', 244, 150, false),
span('e1', 'a1', 'template', 'Render order.json', 396, 14)
];
const KINDS: TimelineKind[] = [
{ id: 'db', label: 'Database' },
{ id: 'http', label: 'Network' },
{ id: 'code', label: 'Code' }
];
const TIMEOUT = START + 394;
const TOTAL = 412;
function SpanDetail({ span: detail }: { span: TraceSpan }) {
const facts = [
{ name: 'Operation', value: detail.op },
{ name: 'Starts at', value: `+${formatPreciseDuration(detail.start - START)}` },
{ name: 'Duration', value: `${formatPreciseDuration(detail.duration)}, ${formatPercent((detail.duration / TOTAL) * 100)} of the trace` },
{ name: 'Status', value: detail.ok ? 'OK' : 'Failed' },
{ name: 'Span id', value: detail.id }
];
return (
<>
<p className="mb-3 font-mono text-code break-all text-text">{detail.label}</p>
<dl className="grid grid-cols-2 gap-x-6 gap-y-2 text-xs @xl:grid-cols-3 @3xl:grid-cols-5">
{facts.map((fact) => (
<div key={fact.name} className="min-w-0">
<dt className="text-text-muted">{fact.name}</dt>
<dd className="truncate text-text tabular-nums">{fact.value}</dd>
</div>
))}
</dl>
</>
);
}
export default function TraceWaterfallDemo() {
return (
<div className="w-full overflow-hidden rounded-lg border border-border">
<Timeline.Root
data={SPANS}
kinds={KINDS}
label="Spans of GET /api/orders/1842"
end={START + TOTAL}
defaultValue="d1"
defaultExpanded={['a1', 'c1']}
>
<Timeline.Legend />
<Timeline.Axis heading="Span">
<Timeline.Moment at={TIMEOUT} label="SHOP-212 Payment status timed out" />
</Timeline.Axis>
<Timeline.Rows
label={(row: TraceSpan) => (
<>
<span className="shrink-0 text-xs text-text-muted">{row.op}</span>
<span className="min-w-0 truncate font-mono text-code text-text">{row.label}</span>
</>
)}
detail={(row: TraceSpan) => <SpanDetail span={row} />}
/>
</Timeline.Root>
</div>
);
}import { Timeline, type TimelineKind } from '@adecore/charts';
const KINDS: TimelineKind[] = [
{ id: 'db', label: 'Database' },
{ id: 'http', label: 'Network' },
{ id: 'code', label: 'Code' }
];
<Timeline.Root data={spans} kinds={KINDS} label="Spans of GET /api/orders/1842" value={selected} onValueChange={setSelected}>
<Timeline.Legend />
<Timeline.Axis heading="Span">
{errors.map((error) => (
<Timeline.Moment key={error.id} at={error.at} label={error.title} />
))}
</Timeline.Axis>
<Timeline.Rows
label={(span: TraceSpan) => (
<>
<span className="shrink-0 text-xs text-text-muted">{span.op}</span>
<span className="min-w-0 truncate font-mono text-code text-text">{span.label}</span>
</>
)}
detail={(span: TraceSpan) => <SpanDetail span={span} />}
/>
</Timeline.Root>A timeline is no Chart.Root: its rows are HTML, its height follows the rows, and the keyboard moves through spans rather than buckets. It sits flush in a panel, with its own 16px at either side, so a row's background runs from edge to edge. Below 576px of width, the bar of a row goes under its label.
Spans
Each row of data is a TimelineSpan. The app keeps its own fields on it, and the render props of Timeline.Rows hand the app's type back when their parameter names it.
| Field | Type | |
|---|---|---|
id | string | Required. |
parent | string | null | The span it is part of. Null, or an id that is not in data, makes it a root. |
label | string | Required. The name a screen reader reads, and what the row shows without a label render prop. |
start, duration | number | Required. Milliseconds, on the same clock as every other span and moment. |
kind | string | The id of one of kinds. |
tone | ChartColor | Stands in for the color of its kind, such as 'error' for a span that failed. |
The rows follow the tree: every span after its parent, and siblings by start. A span in a parent cycle starts a tree of its own.
Timeline.Root
The axis runs from the earliest start to the latest end, or to end where the app fixes it, such as at the end of the root; a span that runs past end is cut off there. kinds lists what a span can wait on, as TimelineKinds (id, label, tone); a kind without a tone takes the next chart color, and a span without a kind the accent.
| Prop | Type | Default | |
|---|---|---|---|
data | readonly Span[] | Required. | |
label | string | Required. The name of the tree for a screen reader. | |
kinds | readonly TimelineKind[] | [] | |
value, defaultValue, onValueChange | string | null | null | The selected span, which shows its detail. Selecting it again clears it. |
expanded, defaultExpanded, onExpandedChange | readonly string[] | every span with children | The spans that show their children. |
format | (ms: number) => string | formatPreciseDuration | Writes every duration. The default of @adecore/ui/format writes 0.4 ms, 123 ms and 1.42 s, and from a minute on as formatDuration does. |
ticks | number | 5 | The most steps on the axis. |
end | number | the latest end | Where the axis ends, in milliseconds on the clock of the spans. |
className, ref | On the outer box. |
TimelineRootProps are the props.
Legend, axis and moments
Timeline.Legend writes each kind with its color and the time its spans take together. The roots are left out, as they cover the whole trace, and so is a kind without time.
Timeline.Axis writes heading over the labels and the ticks over the bars. The ticks are round steps of 1, 2 or 5 from zero; durationTicks(total, count) returns them, for an axis of durations of the app's own, such as a linear Chart.Root with an XAxis that takes them.
Timeline.Moment goes inside the axis: a diamond at at, in the error tone unless tone says otherwise, with a tooltip that names it and its offset from the start ("SHOP-212 Payment status timed out at +394 ms"). A moment before the first span or after the last stands at the end of the axis.
TimelineLegendProps, TimelineAxisProps and TimelineMomentProps are the props.
Timeline.Rows
A row per span that is not folded away: the guides of its depth, a button that folds it, a swatch in its color, its label, and its bar with the duration inside, or beside it on the side with room when the bar is narrow. A folded row says how many spans hang directly under it.
| Prop | Type | |
|---|---|---|
label | (span, row) => ReactNode | What the row shows after its swatch; the span's label by default. row is a TimelineRowState: depth, children, expanded and the color of its bar. |
detail | (span) => ReactNode | What the selected row shows under it, in a sunken box across both columns. A press inside it does not clear the selection. |
className, ref | On the tree. |
TimelineRowsProps are the props.
Keyboard and screen reader
The rows are a tree with one tab stop, like Tree in @adecore/ui. Every row is a treeitem with its level, its place among its siblings, whether it is open and whether it is selected. Its name says its label, kind, start and duration ("SELECT orders, Database, starts at +12 ms, takes 40 ms"), and the detail of the selected row is its description.
| Key | |
|---|---|
| Up, Down | The previous or the next row. |
| Home, End | The first or the last row. |
| Right | Opens a folded row, or moves into an open one. |
| Left | Folds an open row, or moves to the parent. |
| Enter, Space | Selects the row, or clears the selection on the selected one. |
Folding a row with the focus somewhere under it hands the tab stop to that row. The moments are each a stop of their own, so their tooltip opens on focus.