Tree Trace Shell

A compound primitive for walking a recursion: a tree pane on the left, a code-trace pane on the right, and an ordered list of steps that pin one tree node and one code line at a time. Click a node, the matching line highlights; click a line, the matching node focuses. The same shell renders fibonacci, divide-and-conquer, backtracking, or any algorithm whose story is the line-by-line execution of a recursive call.

Reach for it when prose explanation isn't enough — when a learner needs to see the call expression and the line of code that produced it light up together.

Recurrence tree rooted at fib(3). 5 visible nodes. Focused: fib(3) = 2.fib(3)2fib(2)1fib(1)1fib(0)0fib(1)1rtv-_R_1b2anpfivabrb_
function fib(n) {
  if (n < 2) return n;
  const a = fib(n - 1);
  const b = fib(n - 2);
  return a + b;
}
Enter the call — n >= 2, recurse on both branches.
Customize
Shape
3
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/tree-trace-shell.json

Usage

import {
  TreeTraceShell,
  type TreeTraceStep,
  type RecurrenceTreeNode,
} from "@craft-bits/core";
 
const nodes: RecurrenceTreeNode[] = [
  { id: "root", label: "fib(3)", value: 2 },
  { id: "l", parentId: "root", label: "fib(2)", value: 1 },
  { id: "r", parentId: "root", label: "fib(1)", value: 1, tone: "resolved" },
];
 
const code = `function fib(n) {
  if (n < 2) return n;
  const a = fib(n - 1);
  const b = fib(n - 2);
  return a + b;
}`;
 
const steps: TreeTraceStep[] = [
  { id: "enter", nodeId: "root", codeLine: 1, caption: "Enter fib(3)." },
  { id: "left",  nodeId: "l",    codeLine: 3, caption: "Left branch." },
  { id: "right", nodeId: "r",    codeLine: 4, caption: "Right branch." },
];
 
<TreeTraceShell
  nodes={nodes}
  code={code}
  lang="ts"
  steps={steps}
  defaultActiveStep="enter"
/>

Drive the active step from outside (for example, wire it to a scrubber):

const [stepId, setStepId] = useState("enter");
 
<TreeTraceShell
  nodes={nodes}
  code={code}
  steps={steps}
  activeStep={stepId}
  onActiveStepChange={setStepId}
/>

Swap in a different tree primitive via the treeSlot escape hatch:

<TreeTraceShell
  nodes={nodes}
  code={code}
  steps={steps}
  treeSlot={<DecisionTreeViz nodes={treeNodes} />}
/>

Understanding the component

  1. One state, two panes. A single activeStep id is the source of truth. Each step in the array carries { id, nodeId, codeLine, caption } — the shell resolves the active step, then forwards the nodeId to the tree pane as focusedId and the codeLine to the code pane as activeLine.
  2. Bidirectional clicks. Clicking a tree node calls the tree's onFocusedIdChange; the shell looks up the first step whose nodeId matches and commits it. Clicking a code line calls the code-trace's onActiveLineChange; the shell looks up the first step whose codeLine matches. Either pane can advance the trace.
  3. Controlled and uncontrolled. Pass activeStep plus onActiveStepChange for controlled mode (drive from an external reducer or scrubber). Pass defaultActiveStep for uncontrolled mode (the shell remembers internally). Omit both and the first step is selected on mount.
  4. Slots, not props. The treeSlot and codeSlot props accept any ReactNode — swap in DecisionTreeViz or CodeBlock without forking the shell. The default panes are RecurrenceTreeViz and CodeTrace, both already in @craft-bits/core.
  5. Step row as scrubber. When steps.length > 0 and showStepRow is true, a horizontal row of numbered chips renders below the panes. Each chip is role="tab" with the active step marked aria-selected="true"; chips meet the 44 px hit-target minimum even when visually smaller.
  6. Reduced motion. Caption transitions collapse to instant under prefers-reduced-motion; the underlying tree and code panes already honour the same preference.

Props

PropTypeDefaultDescription
nodesreadonly RecurrenceTreeNode[]requiredFlat list of tree nodes (RecurrenceTreeViz shape).
codestringrequiredSource code rendered in the code pane.
langCodeTraceLang"tsx"Shiki language id for the code pane.
showLineNumbersbooleantrueRender the 1-indexed line-number gutter.
stepsreadonly TreeTraceStep[][]Ordered list of (node, line) pairs to walk through.
activeStepstring | nullControlled active step id.
defaultActiveStepstring | nullnullUncontrolled initial active step id.
onActiveStepChange(id: string | null) => voidFires whenever the active step changes.
showStepRowbooleantrueRender the numbered step scrubber below the panes.
stackBelow"sm" | "md" | "lg""md"Breakpoint below which panes stack vertically.
treeSlotReactNodeOverride the tree pane (e.g. DecisionTreeViz).
codeSlotReactNodeOverride the code pane (e.g. CodeBlock).
headerReactNodeContent rendered above the panes.
footerReactNodeContent rendered below the panes.
classNamestringMerged onto the outer wrapper via cn().

Accessibility

  • The step scrubber is role="tablist" with each chip role="tab" and the current step marked aria-selected="true".
  • Every scrubber chip clears the 44 x 44 px hit-target floor (WCAG 2.5.8) and shows a visible focus ring.
  • The tree pane inherits RecurrenceTreeViz's role="img" with a descriptive title; every node is keyboard-reachable.
  • The code pane inherits CodeTrace's aria-current="true" on the active line, plus a polite live region that announces highlight changes.
  • Motion respects prefers-reduced-motion: caption crossfades collapse to instant, and both panes already honour the preference internally.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/TreeTraceShell.tsx). The source was lesson chrome — a single layout that wired useTreeTrace, DecisionTreeViz, StepProgress, a backtrack-narration override, a LessonContext-aware replay button, and a pill row for collected items into one component. The library version is the bridge mechanism: tree + code, an ordered step list, and bidirectional click highlighting. Lesson chrome (replay, items pills, narration overrides, track colour) drops out — callers compose it externally via header / footer slots, or skip it entirely.