Call Stack Viz
A vertical function-call stack visualization. Frames are accepted bottom-first — frames[0] is the base of the stack (e.g., main()), frames.at(-1) is the currently-executing frame. New frames push in from the top (or bottom, when direction="bottom-grows"); popped frames glide out the same way via motion's layoutId-driven layout animation. Common for teaching recursion, tail calls, stack overflow, and debugger semantics.
fib(3)
fib(4)
fib(5)
main()
Customize
Stack
4
3
Installation
npx shadcn@latest add https://craftbits.dev/r/call-stack-viz.jsonUsage
import { CallStackViz } from "@craft-bits/core";
<CallStackViz
frames={[
{ id: "main", name: "main", line: 12 },
{ id: "fib3", name: "fib", args: "3", line: 4 },
{ id: "fib2", name: "fib", args: "2", line: 3, tone: "active" },
]}
/>Drive pushes and pops from your own algorithm reducer — the component is pure render:
const [stack, setStack] = useState<CallStackFrame[]>([]);
<CallStackViz frames={stack} showLocals />
<button onClick={() => setStack((s) => [...s, { id: nextId(), name: "fib", args: "3" }])}>
push
</button>
<button onClick={() => setStack((s) => s.slice(0, -1))}>
pop
</button>Understanding the component
- Bottom-first input, top-first render (by default). The
framesarray is read base-first —frames[0]is the deepest frame,frames.at(-1)is the active frame. The DOM reverses to put the most-recent frame at the visual top, matching the textbook stack diagram. Flip withdirection="bottom-grows"to mirror Chrome DevTools, where the most-recent frame is listed last. layoutId-driven motion. Every frame carrieslayoutId={frame.id}andlayout, so pushes / pops animate as glides instead of remount-and-fade. New frames enter withtranslateY(±8) + scale(0.97) + opacity 0from the push side; popped frames exit the same direction.SPRINGS.smoothdrives the transition.- Per-frame tone. Each
frames[i].toneis one ofdefault,active,returning,dimmed. Tones resolve to inline CSS-var styles (--cb-accent,--cb-success,--cb-bg-muted) so consumer themes repaint the stack without re-rendering. - Optional locals snapshot. When
showLocalsis on and the frame carries alocalsmap, a collapsed key/value panel sits beneath the header. The panel toggles via a<button aria-expanded>so keyboard + screen-reader users get full access. - Middle truncation. When
maxVisibleis set and the stack exceeds it, the middle frames collapse into a single dashed… +N moremarker. The base frame and the top of the stack both stay visible — this matches how stack-trace UIs typically truncate, and preserves "where we are" plus "what we started from." - Reduced motion. When
prefers-reduced-motion: reduceis set, the spring collapses to{ duration: 0 }and frames snap in and out.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
frames | readonly CallStackFrame[] | required | Stack frames, bottom-first. frames[0] is the base, frames.at(-1) is the top. |
direction | "top-grows" | "bottom-grows" | "top-grows" | Which way newer frames push. |
showLocals | boolean | false | Render each frame's locals snapshot as an expandable panel. |
maxVisible | number | — | Max visible frames — middle collapses into a +N marker when exceeded. |
className | string | — | Merged onto the outer <div>. |
CallStackFrame
| Field | Type | Description |
|---|---|---|
id | string | number | Stable React + layoutId key. Must be unique within the stack. |
name | string | Function name (rendered in monospace). |
args | ReactNode | Optional argument summary rendered inside the parentheses. |
line | number | Optional active line number — surfaces a @ line N chip. |
locals | Record<string, unknown> | Optional locals snapshot rendered in an expandable panel. |
tone | "default" | "active" | "returning" | "dimmed" | Semantic tone. Defaults to "default". |
Accessibility
- The stack body is
role="list"witharia-live="polite", and itsaria-labelis rebuilt on every render ("Call stack with 4 frames. Top: fib.") so SR users hear what changed at the top after every push or pop. - Each frame is
role="listitem"with its ownaria-label("Frame #3: fib(2) at line 3"), carriesdata-state="default" | "active" | "returning" | "dimmed"for testing selectors, and tags the top frame withdata-top="true". - The locals-expand control is a real
<button>witharia-expanded— fully keyboard- and screen-reader-accessible. - Color is never the only signal:
activeandreturningtones each add an inset ring,dimmedreduces opacity, and every frame still renders its name + args in monospace. - Motion respects
prefers-reduced-motion: enter / exit / layout transitions collapse to{ duration: 0 }so the stack snaps.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/viz/CallStackViz.tsx). The original primitive was a single-column highlight of recursion depth keyed on a per-tracktrackHexand anisStaleflag, used to dim the previous stack while the next answer was being authored. The library extract generalizes that surface: tones come from the--cb-*token vocabulary, frames carry structuredargs+line+locals, and pushes / pops animate vialayoutIdglides instead of fade-only transitions.