Staircase Builder

A self-contained <svg> staircase. Pass a rows array of bar heights and the component lays out one vertical bar per entry, baseline-aligned, with the integer value hovering above each settled bar and an optional category label below the baseline. Steps before currentRow render as settled tone-filled bars; the step at currentRow pulses; steps after currentRow render as dashed empty placeholders so the silhouette of the finished staircase is visible from step 1.

Designed for cumulative / DP-style visualisations — prefix sums, monotonic accumulations, scanline counters, geometric progressions — where the visual point is which step is settled, which step is the algorithm writing right now, and how much more is still to build. The primitive carries no algorithm: pass any sequence of heights and the caller decides what onRowSelect means.

Staircase with 9 steps, 5 settled (values 0, 3, 4, 8, 9).0031428394145678
Customize
State
5
Layout
32
120
Style
1

Installation

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

Usage

import { StaircaseBuilder } from "@craft-bits/core";
 
<StaircaseBuilder rows={[0, 3, 4, 8, 9, 14, 23, 25, 31]} currentRow={5} />

Controlled — parent owns the current step, click any bar to jump:

const [currentRow, setCurrentRow] = useState(0);
 
<StaircaseBuilder
  rows={prefix}
  currentRow={currentRow}
  onCurrentRowChange={setCurrentRow}
/>

Uncontrolled — the component owns the current step:

<StaircaseBuilder rows={prefix} defaultCurrentRow={3} />

Read-only static staircase (no click handlers, no focusable bars):

<StaircaseBuilder rows={prefix} editable={false} currentRow={prefix.length} />

Label every step and surface the running total via onRowSelect:

<StaircaseBuilder
  rows={prefix}
  labels={prefix.map((_, i) => String(i))}
  onRowSelect={(idx) => console.log("selected step", idx)}
/>

Understanding the component

  1. Self-contained SVG. The component renders one <svg> sized to fit every bar plus the value and axis labels. The caller positions it; no top / left / transform: translate is set internally.
  2. Three bar states. Bars at index < currentRow are settled (tone-filled, value visible); the bar at currentRow is active (brighter fill plus a 1.2s breathing border); bars at index > currentRow are empty (dashed outline at the proportional height of their value, no fill).
  3. Empty silhouette. Empty bars render at the height they will eventually settle to, so the staircase ghost is visible from step 1. The student sees the shape they are building rather than a blank canvas.
  4. Controlled + uncontrolled. currentRow + onCurrentRowChange is the Radix controlled pattern; defaultCurrentRow makes the component own its own state. Pass -1 for "no step active" (every bar empty) or rows.length for "complete" (every bar settled).
  5. Click + keyboard. Every editable bar is a focusable button with Space / Enter activation. Clicking fires onCurrentRowChange(idx) and onRowSelect(idx) — pair them for picker semantics (range-query endpoint selection) or use only the first for plain advance.
  6. Five tones. default reads as neutral count, accent as "currently relevant", success as winning value, warning as watch this bar, error as blown the budget. Tone applies to settled and active bars; empty bars stay neutral so the staircase silhouette never competes with the active step.
  7. Hit target. Each editable bar carries an invisible 44px-wide hit rectangle behind it so narrow bars still satisfy WCAG 2.5.8 — an 8px bar on a 2px gap is still safely tappable on mobile.
  8. Reduced motion. Bar enter animation, value-change spring, and the active-bar pulse all collapse to instant under prefers-reduced-motion: reduce. Values still update; only the motion drops.

Props

PropTypeDefaultDescription
rowsreadonly number[]requiredBar heights, one per staircase step.
currentRownumberControlled current step. Bars before are settled, equal pulses, after are empty.
defaultCurrentRownumber0Uncontrolled initial current step.
onCurrentRowChange(next: number) => voidFires when the current step changes via interaction.
onRowSelect(rowIndex: number) => voidFires when a bar is clicked or activated.
maxValuenumbermax of rowsMaximum value used for height scaling.
tone"default" | "accent" | "success" | "warning" | "error""accent"Settled + active palette.
editablebooleantrueWhen false, bars are not clickable or focusable.
showValuesbooleantrueRender the integer value above each non-empty bar.
labelsreadonly string[]Optional axis labels rendered below each bar.
barWidthnumber32Bar width in pixels.
barGapnumber4Gap between bars in pixels.
plotHeightnumber120Plot area height in pixels.
transitionTransitionSPRINGS.smoothOverride bar transitions. Reduced-motion users snap regardless.
classNamestringMerged onto the <svg> root via cn().

Accessibility

  • The outer <svg> is role="img" with a <title> summarising the step count and settled values. Screen readers hear the staircase without parsing the SVG geometry.
  • Every editable bar is role="button" with tabIndex={0}, an explicit aria-label naming the step index, settled / active / empty state, and the value. Space and Enter activate the bar.
  • The active bar carries aria-current="step" so assistive tech announces the current write position alongside the bar's value.
  • Each bar carries an invisible 44px-wide hit rectangle so narrow bars still satisfy WCAG 2.5.8 AAA on touch screens.
  • The component exposes data-editable on the root and data-state (settled / active / empty) plus data-tone on every bar group so consumer apps can hook custom styles or assistive tooling.
  • Tone is never the only signal — the value label renders on every non-empty bar regardless of tone, the active bar layers a continuous border pulse beneath the colour change, and empty bars render with a dashed outline.
  • Motion respects prefers-reduced-motion: reduce — the enter stagger, the value spring, and the active-bar pulse all collapse to instant.

Credits

  • Extracted from: AlgoFlashcards (src/lessons/primitives/construction/StaircaseBuilder.tsx). The source was a 1400-line five-phase lesson reducer (GRIND, BUILD, HARVEST, BRIDGE, COMPLETE) bundling prefix-sum prediction gates, range-query op counters, code-bridge fill-the-blank validation, audio cues, progressive hints, and per-phase scoring around a small staircase SVG. The library extract keeps only the visualisation primitive — a sequence of bar heights, controlled / uncontrolled current step, tap-to-select bars, optional value + axis labels — and lets the caller compose any reducer, scoring, prediction gate, or code bridge on top.