Structural Search

A step-through viewer for a search across a structured space — a decision tree, an oracle, a state-search graph. The caller supplies the nodes (each with an id, label, optional parent and edge label) and the path of node ids the search visits in order. The component lays the nodes out as a top-down tree, draws edges between parents and children, highlights the prefix of the visited path, pulses the current node, and surfaces the per-step narration.

Pure layout / playback primitive — it does not run a search, score, or decide. The caller authors the nodes and the path; the component renders them. Drives the canonical "follow the decision tree" lesson (binary-search oracle, alpha-beta game tree, DPLL solver, A* on a small grid) and any side-by-side "what the structure looks like vs. which branch we chose" narration.

Step 1 of 3. Visiting lo <= mid. Start at the oracle root. Decide which half of the array is sorted.
Binary-search oracle
true
false
in
out
in
out
lo <= mid
left sorted
right sorted
discard R
discard L
discard L
discard R

Start at the oracle root. Decide which half of the array is sorted.

1 / 3
Customize
Highlight
1

Installation

npx shadcn@latest add https://craftbits.dev/r/structural-search.json

Usage

import {
  StructuralSearch,
  type StructuralSearchNode,
  type StructuralSearchStep,
} from "@craft-bits/core";
 
const nodes: StructuralSearchNode[] = [
  { id: "root", label: "arr[lo] <= arr[mid]" },
  { id: "true", label: "left sorted", parent: "root", edgeLabel: "true" },
  { id: "false", label: "right sorted", parent: "root", edgeLabel: "false" },
];
 
const path: StructuralSearchStep[] = [
  { nodeId: "root", description: "Start at the oracle root." },
  { nodeId: "true", description: "arr[lo] <= arr[mid] holds." },
];
 
<StructuralSearch nodes={nodes} path={path} />

Controlled — the parent owns the step and can drive it from a prediction gate or an autoplay timer:

const [step, setStep] = useState(0);
 
<StructuralSearch
  nodes={nodes}
  path={path}
  step={step}
  onStepChange={setStep}
/>

Hide the built-in prev / next controls when the step is driven entirely from outside:

<StructuralSearch nodes={nodes} path={path} step={step} hideControls />

Each node can carry an edgeLabel rendered on the edge from its parent — useful for branch labels like true / false, or in-range / out:

const nodes: StructuralSearchNode[] = [
  { id: "root", label: "compare" },
  { id: "lt", label: "go left", parent: "root", edgeLabel: "lt" },
  { id: "ge", label: "go right", parent: "root", edgeLabel: "ge" },
];

Understanding the component

  1. Pure structural replay. The component is fed two arrays: nodes (the search space) and path (the ordered ids visited). The same triple of nodes, path, and step always produces the same view — there is no internal search algorithm, scoring, or branching logic.
  2. Topology from parent references. Each node references its parent by id; order inside nodes does not matter. The layout walks parents to derive depth, then arranges nodes left-to-right within each depth row using a fixed cell size and a gap.
  3. Visited prefix highlight. Every node with an index in path[0..step] is painted with the active tone. Edges between two visited nodes go from thin and muted to thick and tone-coloured; everything else stays neutral.
  4. Current-node pulse. The node at the active step carries a subtle sonar pulse and a 1.5-px tone-coloured ring. The pulse collapses to instant under prefers-reduced-motion: reduce.
  5. Edge labels. When a node declares edgeLabel, the label renders centred on the edge from its parent in a small mono chip. The chip adopts the active tone when both endpoints are on the visited path.
  6. Controlled and uncontrolled step. Pair step with onStepChange for Radix-style control, or pass defaultStep and let the component own its step. Built-in prev / next chrome can be hidden when a parent drives the step from outside.
  7. Step dots. A 10-px dot per step doubles as a jump-to control. Dots before the active one render filled and dimmed; the active one renders at 1.15-scale and full opacity. Each dot's hit area expands to 44 by 44 px.
  8. Keyboard. Arrow-left / arrow-right on the root advance the step. Step dots and prev / next buttons are real buttons with full focus, Enter, and Space support.
  9. Reduced motion. Edge fades, node pulse, narration cross-fade, prev / next tap scale, and dot scale all collapse to instant under prefers-reduced-motion: reduce. The search still advances; only the motion drops.

Props

PropTypeDefaultDescription
nodesreadonly StructuralSearchNode[]requiredNodes in the search space. Each declares an id, label, optional parent, and optional edge label.
pathreadonly StructuralSearchStep[]requiredOrdered route through the structure. Step i visits the node at path[i].nodeId.
stepnumberControlled current step. Pair with onStepChange.
defaultStepnumber0Uncontrolled initial step.
onStepChange(next: number) => voidFires when the active step changes.
tone"default" | "accent" | "success" | "warning" | "error""accent"Highlight palette for the path, the current-node pulse, and the edge accent.
titleReactNodeOptional title rendered above the tree.
prevLabelstring"Prev"Label for the previous-step button.
nextLabelstring"Next"Label for the next-step button.
hideControlsbooleanfalseHide the built-in prev / next chrome.
transitionTransitionSPRINGS.smoothOverride transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the root via cn().

Accessibility

  • A visually-hidden aria-live="polite" region narrates the current step, the visited node's label, and the per-step description so screen readers stay current as the search advances.
  • The root carries aria-roledescription="structural search" plus data-state and data-tone attributes so assistive tooling and consumer styles can hook into the search state.
  • The tree renders inside an <svg role="img"> with an aria-label, so the structure is announced as a single image rather than a stream of decorative shapes.
  • Each step dot is a real <button> with an explicit aria-label and an aria-current="step" flag on the active one. The visible dot is 10 by 10 px; the hit pad expands to 44 by 44 px to satisfy WCAG 2.5.8 AAA.
  • Each rendered node carries data-current and data-visited attributes so styles can react to traversal state without relying on tone hue alone. Current vs. visited vs. resting is also signalled by stroke weight and a sonar pulse, not by colour alone.
  • Prev / next buttons render at 44 by 44 px minimum, disable themselves at the ends, and respond to keyboard Enter and Space. Arrow-left / arrow-right keys on the root advance the step too.
  • Motion respects prefers-reduced-motion: reduce — edge fades, node pulse, narration cross-fade, prev / next tap scale, and dot scale collapse to instant. The search still advances; only the motion drops.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/construction/StructuralSearch.tsx). The source was a 2,400-line six-phase Discardability Engine game (Feel → Build → Stress Test → Rewire → Code Bridge → Completion) that taught binary search as a "discard half" predicate by drag-building two decision-tree oracles, stress-testing them across five scenarios with prediction gates, watching the rotated-array oracle fail on a peak array, then rewiring it and mapping the oracle nodes back to real code. The library extract drops the game, the reducer, the scoring, the audio, and the prediction gates. What remains is the structural spine: a tree of nodes laid out top-down, a stepwise path through them, a visited-prefix highlight, and a current-node pulse. Consumers compose any narrative on top.