Stack Viz

A vertical LIFO stack visualization. Items are accepted bottom-to-topitems[0] is the deepest cell, items.at(-1) is the top. Pushes slide in from above; pops fade up out of view. The topmost item is accent-tinted and labelled with a floating ← top by default.

16
9
4
1
Customize
Stack
4
← top

Installation

npx shadcn@latest add https://craftbits.dev/r/stack-viz.json

Usage

import { StackViz } from "@craft-bits/core";
 
<StackViz items={[1, 4, 9, 16]} />

Animate pushes and pops by updating the items array — the component diffs by tail-position and animates the difference:

const [stack, setStack] = useState<number[]>([1, 4, 9]);
 
<StackViz items={stack} />
<button onClick={() => setStack((s) => [...s, s.length + 1])}>push</button>
<button onClick={() => setStack((s) => s.slice(0, -1))}>pop</button>

Understanding the component

  1. Bottom-to-top input, top-to-bottom render. The items array is read with items[0] at the bottom — the natural way to think about a LIFO. Internally the array is reversed for render so the latest push sits at the top of the DOM, where it visually lands.
  2. Top item is the protagonist. The cell at items.at(-1) is tinted with --cb-accent, gets a soft accent shadow, carries data-state="top", and renders the floating topLabel to its right. Every other cell uses --cb-bg + --cb-border-muted.
  3. Push / pop motion. Cells live inside <AnimatePresence initial={false}>. New cells enter with opacity: 0, y: -cellSize/2, scale: 0.96 — they slide down from above. Popped cells exit with the mirrored transform so they read as "lifting off the top." SPRINGS.smooth drives both directions.
  4. Reduced motion. When prefers-reduced-motion: reduce is set, the spring collapses to duration: 0 and items snap.
  5. Overflow. Without maxHeight, the column grows freely. With maxHeight, the inner region becomes scrollable so excess items remain reachable.
  6. Screen-reader announcement. The stack body is role="list" with aria-live="polite", and its aria-label is rebuilt on every render ("Top of stack: 16. 4 items."), so SR users hear what changed at the top after a push or pop.

Props

PropTypeDefaultDescription
itemsreadonly (string | number)[]requiredStack contents, bottom-to-top. The last element is the top.
cellSizenumber40 (or 32 when compact)Pixel height of each cell, also used as floor min-width.
compactbooleanfalseTighter gaps + smaller default cell.
topLabelstring"← top"Text rendered beside the top item. Pass an empty string to suppress.
maxHeightnumberCap the visible height in pixels. Overflow becomes scrollable.
classNamestringMerged onto the outer <div>.

Accessibility

  • The stack body is role="list" and every cell is role="listitem" — assistive tech sees a single list whose order matches the visual order (top first).
  • aria-live="polite" on the stack region announces the new top after any push or pop. The label is built from the top value, not the position.
  • The top cell carries data-state="top" so consumers can target it via attribute selectors without parsing classes.
  • The floating topLabel is aria-hidden — the live region already names the top, so duplicating it would announce twice.
  • Motion respects prefers-reduced-motion: enter / exit transitions collapse to duration: 0 so the layout snaps.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/StackViz.tsx). The original primitive bundled scan-trail highlighting, multi-stack layoutId morphing, and a per-item hex override targeted at AF's track-color system. The library extract is the LIFO primitive — the lesson-specific affordances live in the source project.