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.
Installation
npx shadcn@latest add https://craftbits.dev/r/staircase-builder.jsonUsage
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
- Self-contained SVG. The component renders one
<svg>sized to fit every bar plus the value and axis labels. The caller positions it; notop/left/transform: translateis set internally. - Three bar states. Bars at index
< currentRoware settled (tone-filled, value visible); the bar atcurrentRowis active (brighter fill plus a 1.2s breathing border); bars at index> currentRoware empty (dashed outline at the proportional height of their value, no fill). - 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.
- Controlled + uncontrolled.
currentRow+onCurrentRowChangeis the Radix controlled pattern;defaultCurrentRowmakes the component own its own state. Pass-1for "no step active" (every bar empty) orrows.lengthfor "complete" (every bar settled). - Click + keyboard. Every editable bar is a focusable button with Space / Enter activation. Clicking fires
onCurrentRowChange(idx)andonRowSelect(idx)— pair them for picker semantics (range-query endpoint selection) or use only the first for plain advance. - Five tones.
defaultreads as neutral count,accentas "currently relevant",successas winning value,warningas watch this bar,erroras blown the budget. Tone applies to settled and active bars; empty bars stay neutral so the staircase silhouette never competes with the active step. - 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.
- 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
| Prop | Type | Default | Description |
|---|---|---|---|
rows | readonly number[] | required | Bar heights, one per staircase step. |
currentRow | number | — | Controlled current step. Bars before are settled, equal pulses, after are empty. |
defaultCurrentRow | number | 0 | Uncontrolled initial current step. |
onCurrentRowChange | (next: number) => void | — | Fires when the current step changes via interaction. |
onRowSelect | (rowIndex: number) => void | — | Fires when a bar is clicked or activated. |
maxValue | number | max of rows | Maximum value used for height scaling. |
tone | "default" | "accent" | "success" | "warning" | "error" | "accent" | Settled + active palette. |
editable | boolean | true | When false, bars are not clickable or focusable. |
showValues | boolean | true | Render the integer value above each non-empty bar. |
labels | readonly string[] | — | Optional axis labels rendered below each bar. |
barWidth | number | 32 | Bar width in pixels. |
barGap | number | 4 | Gap between bars in pixels. |
plotHeight | number | 120 | Plot area height in pixels. |
transition | Transition | SPRINGS.smooth | Override bar transitions. Reduced-motion users snap regardless. |
className | string | — | Merged onto the <svg> root via cn(). |
Accessibility
- The outer
<svg>isrole="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"withtabIndex={0}, an explicitaria-labelnaming 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-editableon the root anddata-state(settled/active/empty) plusdata-toneon 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.