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).
Installation
npx shadcn@latest add https://craftbits.dev/r/monotonic-stack-builder.jsonUsage
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
- 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 exportedderiveMonotonicStackPlanhelper. No internal history, no race conditions, no off-by-one drift when the parent jumpssteparound. - 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. - Shared-layout morph. Every input cell carries a stable
layoutId. Whenstepadvances and the value is pushed onto the stack, the cell mounts in the stack column under the samelayoutId, so Framer Motion morphs the position in a single spring instead of an unmount-remount hard cut. - 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.
- 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.
- Five tones.
defaultreads as "neutral build";accentas "currently relevant" (the default);successas "good push";warningas "watch this head";erroras "constraint violation". The tone paints only the current input cell, the top of the stack, and the popped-this-step tray. - Hit target. The current input cell is at least 44 by 44 px regardless of the
cellSizeprop, so narrow visual cells still satisfy WCAG 2.5.8 on touch screens. - 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
| Prop | Type | Default | Description |
|---|---|---|---|
values | number[] | required | Input sequence to feed into the stack one value at a time. |
direction | "decreasing" | "increasing" | "decreasing" | Stack invariant; selects next-greater or next-smaller behaviour. |
step | number | — | Controlled progress. Pair with onStepChange. |
defaultStep | number | 0 | Uncontrolled initial step. |
onStepChange | (next: number) => void | — | Fires with the next step whenever the build advances. |
interactive | boolean | true | When 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. |
inputLabel | string | "Input" | Column header for the input row. |
stackLabel | string | "Stack" | Column header for the stack column. |
cellSize | number | 44 | Cell width / height in pixels. |
cellGap | number | 4 | Gap between cells in pixels. |
transition | Transition | SPRINGS.smooth | Override cell transitions. Reduced-motion users snap regardless. |
className | string | — | Merged 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 explicitaria-labelnaming its index, value, and current state (current, processed-and-on-stack, popped, waiting), plusaria-pressedto flag the next-pick head. - Space / Enter keyboard activation mirrors click-to-advance; non-tappable cells are removed from the tab order via
tabIndexof-1. - The current input cell is at least 44 by 44 px regardless of the
cellSizeprop 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, anddata-tone; every cell exposesdata-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.