---
title: Waterfall
description: Spans of a trace on its time axis, as a tree.
---

```tsx
import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallBasic() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        <Waterfall.Ruler />
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            label={`${span.duration} ms`}
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}
```

- Use it for one trace when its hierarchy, timing and selected span matter together.
- Use one time scale: row `start` and marker `at` are milliseconds from the trace start, and every `duration` uses milliseconds on that same scale.
- Keep rows in parent/child order and set `depth` to match the hierarchy.
- The root span is `tone="root"`, ordinary spans are `child`, and the failing span is `failing`, which also colours its name.
- Give `Waterfall.Root` an `aria-label`. It is a `tree`, and the rows are one tab stop: ↑ ↓ move, → expands, ← collapses, Enter or Space selects.
- A row scrolls into view when it becomes selected, so selection driven from outside the tree, such as a page shortcut, stays visible.
- The duration label sits after the bar, or before it when the bar ends near the right edge, and is left out when neither side has room.

## Summary

```tsx
import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallSummary() {
  return (
    <Preview block={true}>
      <Waterfall.Summary
        segments={TRACE.spans
          .filter((span) => span.depth === 1)
          .map((span) => ({ id: span.name, duration: span.duration }))}
      />
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}
```

Put a `Waterfall.Summary` above the waterfall to show where the request spent time. Each segment is as wide as its share of the total.

## Expandable rows

```tsx
import { Waterfall } from "@openperf/deft";
import { useState } from "react";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

const PARENTS = new Set(["Span 1", "Span 3"]);

export default function WaterfallExpandableRows() {
  const [collapsed, setCollapsed] = useState<string[]>([]);

  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans
          .filter((span, index) =>
            TRACE.spans
              .slice(0, index)
              .every(
                (ancestor, ancestorIndex) =>
                  !collapsed.includes(ancestor.name) ||
                  TRACE.spans
                    .slice(ancestorIndex + 1, index + 1)
                    .some((between) => between.depth <= ancestor.depth),
              ),
          )
          .map((span) => (
            <Waterfall.Row
              depth={span.depth}
              duration={span.duration}
              expanded={PARENTS.has(span.name) ? !collapsed.includes(span.name) : undefined}
              key={span.name}
              name={span.name}
              onToggle={() => {
                setCollapsed(
                  collapsed.includes(span.name)
                    ? collapsed.filter((name) => name !== span.name)
                    : [...collapsed, span.name],
                );
              }}
              start={span.start}
              tone={span.tone}
            />
          ))}
      </Waterfall.Root>
    </Preview>
  );
}
```

Set `expanded` and `onToggle` on parents to show or hide their children. Leave `expanded` undefined on leaves.

## Critical path

```tsx
import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

const CRITICAL_PATH = new Set(["Span 1", "Span 3", "Span 4"]);

export default function WaterfallCriticalPath() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            dimmed={!CRITICAL_PATH.has(span.name)}
            duration={span.duration}
            key={span.name}
            name={span.name}
            start={span.start}
            tone={CRITICAL_PATH.has(span.name) ? "path" : "child"}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}
```

Set `tone="path"` on the spans that determined the trace's latency and `dimmed` on the rest. This is a timing view, separate from failure status: a healthy span can be critical, and a failing span is not promoted to the path automatically.

## Flat density

```tsx
import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallFlatDensity() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" density="flat" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}
```

`density="flat"` is for compact waterfalls embedded in list pages. Rows are shorter and leaves get no chevron space; leave `expanded` undefined on every row to make a static summary rather than an expandable tree.

## Details

```tsx
import { Waterfall } from "@openperf/deft";
import { Fragment, useState } from "react";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallDetails() {
  const [selected, setSelected] = useState<null | string>("Span 3");

  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Fragment key={span.name}>
            <Waterfall.Row
              depth={span.depth}
              duration={span.duration}
              name={span.name}
              onSelect={() => {
                setSelected(selected === span.name ? null : span.name);
              }}
              selected={selected === span.name}
              start={span.start}
              tone={span.tone}
            />
            {selected === span.name && (
              <Waterfall.Details depth={span.depth}>Details</Waterfall.Details>
            )}
          </Fragment>
        ))}
      </Waterfall.Root>
    </Preview>
  );
}
```

`Waterfall.Details` shows what the selected span carries right under its row: stats, an error and its attributes. Render it after the selected row and pass the row's `depth` so it lines up with the name. Selecting the row again should close it. Details are not rows: arrow keys skip them, and clicks inside never select a span.

## Markers

```tsx
import { Waterfall } from "@openperf/deft";

import { Preview } from "../../src/Preview";
import { TRACE } from "./trace";

export default function WaterfallMarkers() {
  return (
    <Preview block={true}>
      <Waterfall.Root aria-label="Spans" duration={TRACE.duration}>
        {TRACE.spans.map((span) => (
          <Waterfall.Row
            depth={span.depth}
            duration={span.duration}
            key={span.name}
            markers={
              span.name === "Span 3"
                ? [
                    { at: 60, id: "log", kind: "log" },
                    { at: 90, id: "error", kind: "error" },
                  ]
                : undefined
            }
            name={span.name}
            start={span.start}
            tone={span.tone}
          />
        ))}
      </Waterfall.Root>
    </Preview>
  );
}
```

Markers show point-in-time events along a span: `log` circles for log entries and `error` diamonds for failures. Position them with `at` so readers see when an event happened without changing the span's duration.

## API

| Prop | Type | Default | Description |
| - | - | - | - |
| `duration` | `number` | - | On Root and Row, in ms. |
| `nameWidth?` | `number` | `300` | On Root. Width of the name column in pixels, capped at a share of the root. |
| `density?` | `"tree" \| "flat"` | `"tree"` | On Root. |
| `name` | `ReactNode` | - | On Row. The span name on the left. |
| `leading?` | `ReactNode` | - | On Row. Before the name, such as a service icon. |
| `label?` | `ReactNode` | - | On Row. Next to the bar, such as the duration. |
| `start` | `number` | - | On Row, ms from the trace start. |
| `depth?` | `number` | `0` | On Row and Details. |
| `tone?` | `"root" \| "child" \| "path" \| "failing"` | `"child"` | On Row. |
| `dimmed?` | `boolean` | `false` | On Row. De-emphasizes spans off the critical path. |
| `expanded?` | `boolean` | - | On Row. Undefined for leaves. |
| `onToggle?` | `() => void` | - | On Row. Expands or collapses its children. |
| `selected?` | `boolean` | `false` | On Row. |
| `onSelect?` | `() => void` | - | On Row. |
| `markers?` | `{ id: string; at: number; kind: "log" \| "error" }[]` | - | On Row. `at` is in ms from the trace start. |
| `formatDuration?` | `(milliseconds: number) => ReactNode` | - | On Ruler. Defaults to milliseconds in the active locale. |
| `segments` | `{ id: string; duration: number; className?: string }[]` | - | On Summary. Each segment is as wide as its share of the total. |
| `errorAt?` | `number` | - | On Summary. Error marker, ms from the trace start. |

Other props of `Root`, `Details` and `Summary` go to the native `div`. Parts: `Root`, `Details`, `Row`, `Ruler`, `Summary`.
