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.
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.jsonUsage
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
- One state, two panes. A single
activeStepid is the source of truth. Each step in the array carries{ id, nodeId, codeLine, caption }— the shell resolves the active step, then forwards thenodeIdto the tree pane asfocusedIdand thecodeLineto the code pane asactiveLine. - Bidirectional clicks. Clicking a tree node calls the tree's
onFocusedIdChange; the shell looks up the first step whosenodeIdmatches and commits it. Clicking a code line calls the code-trace'sonActiveLineChange; the shell looks up the first step whosecodeLinematches. Either pane can advance the trace. - Controlled and uncontrolled. Pass
activeStepplusonActiveStepChangefor controlled mode (drive from an external reducer or scrubber). PassdefaultActiveStepfor uncontrolled mode (the shell remembers internally). Omit both and the first step is selected on mount. - Slots, not props. The
treeSlotandcodeSlotprops accept any ReactNode — swap inDecisionTreeVizorCodeBlockwithout forking the shell. The default panes areRecurrenceTreeVizandCodeTrace, both already in@craft-bits/core. - Step row as scrubber. When
steps.length > 0andshowStepRowis true, a horizontal row of numbered chips renders below the panes. Each chip isrole="tab"with the active step markedaria-selected="true"; chips meet the 44 px hit-target minimum even when visually smaller. - Reduced motion. Caption transitions collapse to instant under
prefers-reduced-motion; the underlying tree and code panes already honour the same preference.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
nodes | readonly RecurrenceTreeNode[] | required | Flat list of tree nodes (RecurrenceTreeViz shape). |
code | string | required | Source code rendered in the code pane. |
lang | CodeTraceLang | "tsx" | Shiki language id for the code pane. |
showLineNumbers | boolean | true | Render the 1-indexed line-number gutter. |
steps | readonly TreeTraceStep[] | [] | Ordered list of (node, line) pairs to walk through. |
activeStep | string | null | — | Controlled active step id. |
defaultActiveStep | string | null | null | Uncontrolled initial active step id. |
onActiveStepChange | (id: string | null) => void | — | Fires whenever the active step changes. |
showStepRow | boolean | true | Render the numbered step scrubber below the panes. |
stackBelow | "sm" | "md" | "lg" | "md" | Breakpoint below which panes stack vertically. |
treeSlot | ReactNode | — | Override the tree pane (e.g. DecisionTreeViz). |
codeSlot | ReactNode | — | Override the code pane (e.g. CodeBlock). |
header | ReactNode | — | Content rendered above the panes. |
footer | ReactNode | — | Content rendered below the panes. |
className | string | — | Merged onto the outer wrapper via cn(). |
Accessibility
- The step scrubber is
role="tablist"with each chiprole="tab"and the current step markedaria-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'srole="img"with a descriptive title; every node is keyboard-reachable. - The code pane inherits
CodeTrace'saria-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 wireduseTreeTrace,DecisionTreeViz,StepProgress, a backtrack-narration override, aLessonContext-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 viaheader/footerslots, or skip it entirely.