Skip to content

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:

tsx
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>
    );
}
tsx
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.

FieldType
idstringRequired.
parentstring | nullThe span it is part of. Null, or an id that is not in data, makes it a root.
labelstringRequired. The name a screen reader reads, and what the row shows without a label render prop.
start, durationnumberRequired. Milliseconds, on the same clock as every other span and moment.
kindstringThe id of one of kinds.
toneChartColorStands 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.

PropTypeDefault
datareadonly Span[]Required.
labelstringRequired. The name of the tree for a screen reader.
kindsreadonly TimelineKind[][]
value, defaultValue, onValueChangestring | nullnullThe selected span, which shows its detail. Selecting it again clears it.
expanded, defaultExpanded, onExpandedChangereadonly string[]every span with childrenThe spans that show their children.
format(ms: number) => stringformatPreciseDurationWrites 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.
ticksnumber5The most steps on the axis.
endnumberthe latest endWhere the axis ends, in milliseconds on the clock of the spans.
className, refOn 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.

PropType
label(span, row) => ReactNodeWhat 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) => ReactNodeWhat the selected row shows under it, in a sunken box across both columns. A press inside it does not clear the selection.
className, refOn 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, DownThe previous or the next row.
Home, EndThe first or the last row.
RightOpens a folded row, or moves into an open one.
LeftFolds an open row, or moves to the parent.
Enter, SpaceSelects 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.

Licenses and third-party notices are listed in each package.