Headless primitives for visualizing observability spans as collapsible trace trees, waterfalls, and agent and LLM call timelines.
@assistant-ui/react-o11y is experimental. The API may change without notice.
@assistant-ui/react-o11y provides headless, Radix-style primitives for rendering observability spans as collapsible, indented hierarchical UIs. Use it to build trace inspectors, waterfall debug panels, or LLM call timelines on top of your own span data source.
- Headless — Zero styling opinions, bring your own CSS / Tailwind.
- Composable — Radix-style primitive parts you fully control.
- Tree-aware — Automatic depth, parent / child collapse, time range computation.
- Reactive — Built on the same reactive core as the runtimes, so spans stream into your UI as they change.
Installation
npm install @assistant-ui/react-o11yQuick start
Mount SpanResource with your span data, then render primitives that read from the resource's state:
"use client";
import {
SpanPrimitive,
SpanResource,
type SpanData,
} from "@assistant-ui/react-o11y";
import { useAui, AuiProvider } from "@assistant-ui/store";
function SpanRow() {
return (
<SpanPrimitive.Root className="flex items-center gap-2 py-1">
<SpanPrimitive.Indent />
<SpanPrimitive.CollapseToggle className="size-4 cursor-pointer">
▸
</SpanPrimitive.CollapseToggle>
<SpanPrimitive.StatusIndicator className="size-2 rounded-full data-[span-status=running]:bg-yellow-500 data-[span-status=completed]:bg-green-500 data-[span-status=failed]:bg-red-500" />
<SpanPrimitive.TypeBadge className="rounded bg-muted px-1.5 text-xs" />
<SpanPrimitive.Name className="text-sm" />
</SpanPrimitive.Root>
);
}
export function TraceView({ spans }: { spans: SpanData[] }) {
const aui = useAui({ span: SpanResource({ spans }) });
return (
<AuiProvider value={aui}>
<SpanPrimitive.Children components={{ Span: SpanRow }} />
</AuiProvider>
);
}SpanData is the input shape your code produces (see SpanData). SpanResource enriches it with depth, child counts, time range, and collapse state.
Examples
Status states
data-span-status exposes each span's lifecycle, so running, completed, failed, and skipped are styled with pure CSS.
Collapsible subtrees
SpanPrimitive.CollapseToggle removes a span's descendants from the visible flat list, not just hides them. Click a chevron to collapse a subtree.
Streaming spans
SpanResource re-derives the tree whenever the spans prop changes, so spans stream in and update live.
Anatomy
import {
SpanPrimitive,
SpanResource,
SpanByIndexProvider,
} from "@assistant-ui/react-o11y";
// Top-level: mount SpanResource via useAui
const aui = useAui({ span: SpanResource({ spans }) });
<AuiProvider value={aui}>
{/* Render visible spans (flat-list output respecting collapsed state) */}
<SpanPrimitive.Children components={{ Span: SpanRow }} />
</AuiProvider>;
// Each <SpanRow /> sits inside its own SpanByIndexProvider context and reads via primitives
function SpanRow() {
return (
<SpanPrimitive.Root>
<SpanPrimitive.Indent />
<SpanPrimitive.CollapseToggle />
<SpanPrimitive.StatusIndicator />
<SpanPrimitive.TypeBadge />
<SpanPrimitive.Name />
</SpanPrimitive.Root>
);
}SpanPrimitive.Children produces a flat list of visible spans (collapsed subtrees are excluded). Each rendered child is wrapped in a SpanByIndexProvider so its primitives resolve to that specific span's state.
API reference
SpanResource
Resource that ingests raw span data and exposes a tree-aware reactive state to primitives.
SpanResource({ spans }: { spans: SpanData[] }): ClientOutput<"span">;Mount through useAui({ span: SpanResource({ spans }) }).
The resource computes:
- depth of each span based on parent chain.
- hasChildren flag.
- timeRange (
min/maxacross all spans). - collapse state managed internally; toggles through primitives.
SpanData
Input shape you provide to SpanResource:
| Field | Type | Description |
|---|---|---|
id | string | Unique span identifier. |
parentSpanId | string | null | Parent span id, or null for root spans. |
name | string | Display name. |
type | string | Span category (e.g. llm, tool, http). Used by TypeBadge. |
status | "running" | "completed" | "failed" | "skipped" | Lifecycle state. Used by StatusIndicator. |
startedAt | number | Start timestamp (ms). |
endedAt | number | null | End timestamp (ms), or null if still running. |
latencyMs | number | null | Pre-computed latency, or null if running. |
SpanState
Returned by useAuiState((s) => s.span) inside any span-scoped subtree:
type SpanState = {
id: string;
parentSpanId: string | null;
name: string;
type: string;
status: "running" | "completed" | "failed" | "skipped";
startedAt: number;
endedAt: number | null;
latencyMs: number | null;
depth: number;
hasChildren: boolean;
isCollapsed: boolean;
children: SpanItemState[];
timeRange: { min: number; max: number };
};SpanPrimitive.Root
Container <div> exposing span state via data attributes for styling:
| Attribute | Source |
|---|---|
data-span-id | span.id |
data-span-status | span.status |
data-span-type | span.type |
data-span-depth | span.depth |
data-collapsed | span.isCollapsed |
Accepts all standard <div> props.
SpanPrimitive.Indent
A <div> that adds horizontal padding proportional to span depth.
| Prop | Type | Default | Description |
|---|---|---|---|
baseIndent | number | 8 | Base padding in pixels. |
indentPerLevel | number | 12 | Additional padding per depth level. |
Final paddingLeft = baseIndent + depth * indentPerLevel.
SpanPrimitive.CollapseToggle
A <button> that toggles the current span's collapsed state. Renders only when the span has children. Stops click propagation so it can be nested inside a clickable row.
Exposes data-collapsed for styling. Accepts all standard <button> props.
SpanPrimitive.StatusIndicator
A <span> that exposes data-span-status for status-based styling (color, icon swap, etc.). Renders no built-in glyph; you provide the visual.
<SpanPrimitive.StatusIndicator className="size-2 rounded-full data-[span-status=running]:bg-yellow-500 data-[span-status=completed]:bg-green-500 data-[span-status=failed]:bg-red-500 data-[span-status=skipped]:bg-gray-400" />SpanPrimitive.TypeBadge
A <span> that defaults its children to span.type (override by passing custom children). Exposes data-span-type.
SpanPrimitive.Name
A <span> that defaults its children to span.name. Override by passing custom children.
SpanPrimitive.Children
Iterates over visible child spans and renders each.
Two usage modes:
Render-prop:
<SpanPrimitive.Children>
{({ span }) => <CustomRow span={span} />}
</SpanPrimitive.Children>Component prop (recommended for performance):
<SpanPrimitive.Children components={{ Span: SpanRow }} />The components.Span form is memoized; pass a stable component reference to avoid re-renders on unrelated state changes.
Each child is wrapped in SpanByIndexProvider automatically, so primitives inside SpanRow read that child's state.
SpanByIndexProvider
Lower-level escape hatch. Selects one span by index and provides its scope to descendants:
<SpanByIndexProvider index={0}>
<SpanPrimitive.Root>{/* reads span at index 0 */}</SpanPrimitive.Root>
</SpanByIndexProvider>Most users do not need to use this directly; SpanPrimitive.Children wires it up automatically.
SpanPrimitive.ChildByIndex
Convenience component that pairs SpanByIndexProvider with a render component:
<SpanPrimitive.ChildByIndex index={0} components={{ Span: SpanRow }} />Useful when you want explicit control over which child indices to render (e.g. for virtualization).
SpanPrimitive.Timeline
A <div> that provides a timeline range to descendant TimelineBar primitives and exposes range CSS variables.
| Prop | Type | Default | Description |
|---|---|---|---|
timeRange | { min: number; max: number } | span.timeRange | Override the range used for positioning bars. |
paddingEnd | number | 0 | Extends the max by this ratio of the range duration, useful for right-side breathing room. |
Exposes data-span-timeline and CSS variables:
| Variable | Description |
|---|---|
--span-timeline-min-ms | Timeline start timestamp. |
--span-timeline-max-ms | Timeline end timestamp after padding. |
--span-timeline-range-ms | Timeline duration after padding. |
SpanPrimitive.TimelineBar
A <div> that positions the current span within the nearest SpanPrimitive.Timeline range. It reads startedAt, endedAt, status, type, and timeRange from the current span scope.
<SpanPrimitive.Timeline>
<SpanPrimitive.Children>
{() => (
<div className="relative h-8">
<SpanPrimitive.TimelineBar className="top-1 h-6 rounded bg-blue-500" />
</div>
)}
</SpanPrimitive.Children>
</SpanPrimitive.Timeline>Running spans use the timeline range max when now is omitted, keeping SSR and hydration deterministic. For live ticking, update a timestamp from an effect or animation loop and pass it as now.
| Prop | Type | Default | Description |
|---|---|---|---|
now | number | undefined | Explicit timestamp used as the end of running spans. |
timeRange | { min: number; max: number } | nearest Timeline range | Override the range used for this bar. |
Exposes data-span-status, data-span-type, data-span-running, and these CSS variables:
| Variable | Description |
|---|---|
--span-timeline-left | Bar start position as a percentage. |
--span-timeline-end | Bar end position as a percentage. |
--span-timeline-width | Bar width as a percentage. |
--span-timeline-duration-ms | Bar duration in milliseconds. |
--span-timeline-min-width | Optional consumer-set minimum width used by the default inline size. |
Styling
All primitives forward refs and accept standard DOM props (className, style, etc.). Use the data attributes for state-based styling:
[data-span-status="running"] {
/* yellow indicator */
}
[data-span-status="failed"] {
/* red indicator */
}
[data-collapsed="true"] svg {
transform: rotate(0deg);
}
[data-collapsed="false"] svg {
transform: rotate(90deg);
}