Monotonic Stack Builder

An input row of values sits above a vertical stack column and a popped-this-step tray. Advance the step prop from 0 (nothing processed) to values.length (all processed) and the component replays the canonical monotonic-stack algorithm — pop while the invariant is violated, then push — moving each pushed cell from the input row down into the stack via shared layoutId. Drives next-greater-element discovery games, next-smaller-element span problems, the "largest rectangle in histogram" stack pass, and any teaching surface where the pop cascade is the load-bearing visual.

Controlled via step and onStepChange, uncontrolled via defaultStep. The component is interactive by default — the current input cell is tappable and advances step on click. Disable interactive to drive the build purely from a parent (e.g. an autoplay timer or a parent reducer).

Monotonic decreasing stack, input [2, 1, 5, 6, 2, 3]. Processed 0 of 6. Stack bottom to top: empty. 0 pushes, 0 pops.
Input
Stack
0 / 6
Customize
Layout
44
4
Stack invariant
0
Highlight
1

Installation

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

Usage

import { MonotonicStackBuilder } from "@craft-bits/core";
 
<MonotonicStackBuilder values={[2, 1, 5, 6, 2, 3]} />

Controlled — parent owns the step, tap or auto-advance to build:

const [step, setStep] = useState(0);
 
<MonotonicStackBuilder
  values={[2, 1, 5, 6, 2, 3]}
  step={step}
  onStepChange={setStep}
/>

Strictly increasing stack (next-smaller-element shape):

<MonotonicStackBuilder
  values={[5, 3, 4, 1, 2]}
  direction="increasing"
/>

Read-only autoplay-driven mode — the parent advances step on a timer; the component never accepts taps:

<MonotonicStackBuilder
  values={values}
  step={step}
  interactive={false}
/>

Highlight the active cell in a warning tone for a "watch this" beat:

<MonotonicStackBuilder values={values} tone="warning" />

Understanding the component

  1. Pure step replay. The stack contents, the popped-this-step tray, and the running push / pop counters are all derived from (values, step, direction) alone — the same triple always produces the same plan via the exported deriveMonotonicStackPlan helper. No internal history, no race conditions, no off-by-one drift when the parent jumps step around.
  2. Direction sets the invariant. direction="decreasing" keeps the stack strictly decreasing bottom to top, so a new value pops every entry it is greater than or equal to before pushing itself — the canonical next-greater-element shape. direction="increasing" flips the comparison and gives you the next-smaller-element shape from the same component.
  3. Shared-layout morph. Every input cell carries a stable layoutId. When step advances and the value is pushed onto the stack, the cell mounts in the stack column under the same layoutId, so Framer Motion morphs the position in a single spring instead of an unmount-remount hard cut.
  4. Popped-this-step tray. Cells popped by the most recent push render in a side tray with a struck-through, toned style so the cascade is visible even after the stack has settled. The tray is empty between pushes that did not pop anything.
  5. Equality pops. The strict-monotonic invariant pops equal entries too, so every cell on the stack is a clean witness for the next-greater (or next-smaller) relationship to its right.
  6. Five tones. default reads as "neutral build"; accent as "currently relevant" (the default); success as "good push"; warning as "watch this head"; error as "constraint violation". The tone paints only the current input cell, the top of the stack, and the popped-this-step tray.
  7. Hit target. The current input cell is at least 44 by 44 px regardless of the cellSize prop, so narrow visual cells still satisfy WCAG 2.5.8 on touch screens.
  8. Reduced motion. The shared-layout morph, the cell enter / exit, and the completion chip all collapse to instant under prefers-reduced-motion: reduce. The build still advances; only the motion drops.

Props

PropTypeDefaultDescription
valuesnumber[]requiredInput sequence to feed into the stack one value at a time.
direction"decreasing" | "increasing""decreasing"Stack invariant; selects next-greater or next-smaller behaviour.
stepnumberControlled progress. Pair with onStepChange.
defaultStepnumber0Uncontrolled initial step.
onStepChange(next: number) => voidFires with the next step whenever the build advances.
interactivebooleantrueWhen false, the current cell is not clickable or focusable.
tone"default" | "accent" | "success" | "warning" | "error""accent"Highlight palette for the current cell, the stack top, and the popped tray.
inputLabelstring"Input"Column header for the input row.
stackLabelstring"Stack"Column header for the stack column.
cellSizenumber44Cell width / height in pixels.
cellGapnumber4Gap between cells in pixels.
transitionTransitionSPRINGS.smoothOverride cell transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the root via cn().

Accessibility

  • A visually-hidden aria-live="polite" region narrates the input sequence, the processed count, the current stack contents bottom-to-top, and the running push / pop totals so screen readers stay current as the build advances.
  • The current input cell is a real <button> with an explicit aria-label naming its index, value, and current state (current, processed-and-on-stack, popped, waiting), plus aria-pressed to flag the next-pick head.
  • Space / Enter keyboard activation mirrors click-to-advance; non-tappable cells are removed from the tab order via tabIndex of -1.
  • The current input cell is at least 44 by 44 px regardless of the cellSize prop so narrow cells still satisfy WCAG 2.5.8 AAA on touch screens.
  • The popped-this-step tray is an aria-live="polite" status region so screen readers hear the cascade as it happens, not just the final state.
  • The root exposes data-state (building / complete), data-direction, and data-tone; every cell exposes data-state (current / processed / waiting / top / below) so consumer apps can hook custom styles or assistive tooling.
  • Tone is never the only signal — the current cell and the stack top also gain a thicker inset ring and a contrasting fill, so colourblind users see the highlight even when the tone hue is hard to discriminate.
  • Motion respects prefers-reduced-motion: reduce — the shared-layout morph and the completion chip collapse to instant. The build still advances; only the motion drops.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/construction/MonotonicStackBuilder.tsx). The source was a 2500-line lesson component bundling a thirteen-phase reducer (brute force scan, discovery bridge, stack construction, code bridge, done), audio cues, per-cascade prediction gates, micro-queries, MagicMove code morph, and a per-phase score breakdown. The library extract keeps only the visualisation primitive — input row, monotonic stack column, popped-this-step tray, controlled / uncontrolled, two directions — and lets the caller compose any reducer-driven scoring or scaffolding on top.