Resources

Gautam Anand5 min read

What goes into one drift‑ui component

The trace waterfall installs as two files. Its pull request changed 16. A walk through each one, and the six bugs review caught before merge.

A pull request listing trace waterfall files with their added lines, and a blue card labelled 16 files changed, beside the headline Two files to install. Sixteen to ship.

TL;DR

  • Users install two files. The pull request also touched the registry manifest, docs, agent docs, the keyboard map, tests and the README.
  • Pure logic in a lib file meant 15 of the 22 tests needed no rendering.
  • Review found six bugs, including a two-span parent loop that emptied the whole tree.
  • Insert into every list alphabetically, because a test fails if you append.
On this page

When you run npx shadcn add for the trace waterfall, two files land in your project. The pull request that added it changed 16.

If you’re planning to add a component to drift-ui, the other 14 are the part that surprises people. This post walks through pull request #112 by Syed Fahad Junaid, file by file, and then through the six things review caught before it merged.

The pull request

The trace waterfall shows the spans of one request as a collapsible tree, with a bar for each span on a shared timeline. It’s the view you’d see in Jaeger or Honeycomb when a request is slow and you want to know which call took the time.

The pull request opened on November 8 and merged the next morning. It added 1,589 lines across 16 files. 593 of those lines are the component itself. 556 are tests.

Where the 16 files go

FileWhat it does
registry/default/ui/trace-waterfall.tsxThe component people install, 356 lines
registry/default/lib/trace-waterfall.tsPure functions behind it, 237 lines
registry.jsonTells the shadcn CLI what to install
tests/lib/ and tests/ui/15 tests for the logic, 7 for the rendered component
components/preview/, four example files and preview-registry.tsxThe live demo and examples on the docs page
lib/catalog.tsPuts it in the Structure category
lib/component-keyboard.tsThe keyboard table on the docs page
component-usage.ts and component-api.tsNotes and props for coding agents, served at /llms.txt
README.mdA link in the component table, and the count from 50 to 51

Most of these are one-line additions. catalog.ts got one line and preview-registry.tsx got two. They still matter, because without them the component builds but never shows up on the site.

Why the logic is a separate file

The lib file has no React in it. It turns a flat list of spans into a tree, works out where the timeline starts and ends, picks tick marks and formats durations like 840 µs or 1.2 s.

Splitting it out made most of the testing cheap. 15 of the 22 tests call plain functions with plain data, so they don’t render anything or simulate clicks. The 7 UI tests are left for what only a browser can show, like keyboard focus and expand state.

The registry entry

registry.json
{
  "name": "trace-waterfall",
  "type": "registry:ui",
  "title": "Trace Waterfall",
  "dependencies": ["lucide-react"],
  "registryDependencies": ["utils", "@drift-ui/use-controllable-state"],
  "files": [
    { "path": "registry/default/ui/trace-waterfall.tsx", "type": "registry:ui" },
    { "path": "registry/default/lib/trace-waterfall.ts", "type": "registry:lib" }
  ]
}

dependencies lists npm packages, here only lucide-react for the chevron icons. registryDependencies lists other registry items. shadcn built-ins like utils go in by bare name, and items that only drift-ui has get the @drift-ui/ prefix. Get one of these wrong and the install works on your machine, where the file already exists, and fails on someone else’s.

The keyboard map

A trace is a tree, so the component follows the WAI-ARIA tree view pattern. Up and Down move between visible spans. Right expands a span, and pressing it again moves into the first child. Left does the reverse. Home and End jump to the first and last span, and Enter or Space selects one.

Those five bindings live in component-keyboard.ts as data, and the docs page renders them as a table. If the component and the table disagree, the docs are wrong, so they’re worth checking against each other before you open the pull request.

What review caught

cubic, the review bot on this repo, left six comments on the first version. Fahad fixed all six in one follow-up commit and replied to each one with what changed.

CommentWhat would have happened
Parent cyclesSpans where A’s parent is B and B’s parent is A disappeared from the tree
NaN start or durationBars rendered with a width of NaN%
Start time in the labelA span drawn at 0 ms was announced to screen readers as starting at 100 ms
Focus after clicking the expanderKeyboard focus got stuck on a hidden button
Controlled modeNo test passed in expanded or selected state from outside
attributes fieldThe API reference left out part of the span type

The first one is the most interesting. The code already handled a span that names itself as its parent. It didn’t handle a loop of two. Each span was waiting to be attached under the other, so neither was a root, and the tree came out empty. Real tracing data can do this when span IDs collide.

The fix walks up from each span and stops if it sees an ID twice:

tests/lib/trace-waterfall.test.ts
it("breaks multi-span parent cycles and promotes members to root level", () => {
  const cycleSpans: Span[] = [
    { id: "A", parentId: "B", name: "Span A", start: 0, duration: 10 },
    { id: "B", parentId: "A", name: "Span B", start: 10, duration: 10 },
  ]
  const ids = buildSpanTree(cycleSpans).map((t) => t.span.id)
  expect(ids).toContain("A")
  expect(ids).toContain("B")
})

The label bug came from the same shortcut in two places. The bars were positioned relative to the earliest span, but the accessible name used the raw start time. Sighted users and screen reader users were reading different numbers off the same chart.

If you’re adding one

The full list is in Adding a component in CONTRIBUTING.md. Before you start, read two components close to yours. Fahad’s is a good one to read if you’re building anything tree-shaped, and tags-input.tsx is a good one for inputs.

You don’t need to start with a whole component. A missing keyboard binding or a wrong line in the API reference is a real fix and a smaller first pull request. Our good first issues include both kinds.