Pointer Row
A horizontal row of cells with one or more pointer overlays drawn above them. Every pointer is a record with four fields: a stable id for React identity, the index of the cell it sits on, a short label rendered above that cell, and an optional tone driving the label colour. As the caller drives the index values forward, each pointer tweens to its new cell with a single critically-damped spring. When two pointers share an index, the labels stack vertically inside the same column.
PointerRow is the geometric primitive underneath any algorithm narrative that walks indices over a sequence — two-pointer scans, sliding windows, in-place partitions, binary-search probes. The component owns the row, the pointer lane, and the responsive cell sizing; the caller owns every pointer's identity, position, and semantics.
Installation
npx shadcn@latest add https://craftbits.dev/r/pointer-row.jsonUsage
import { PointerRow } from "@craft-bits/core";
const values = [1, 3, 5, 7, 9, 11, 13, 15];
<PointerRow
values={values}
pointers={[
{ id: "L", index: 0, label: "L", tone: "accent" },
{ id: "R", index: 7, label: "R", tone: "accent" },
{ id: "i", index: 3, label: "i", tone: "warning" },
]}
/>;Drive a two-pointer scan — flip the pointer positions on each frame and the labels tween between cells:
const [l, setL] = useState(0);
const [r, setR] = useState(values.length - 1);
<PointerRow
values={values}
pointers={[
{ id: "L", index: l, label: "L", tone: "accent" },
{ id: "R", index: r, label: "R", tone: "accent" },
]}
/>;Stack pointers on the same cell — when lo and hi collide at the end of a binary search, the labels render in one column:
<PointerRow
values={[1, 2, 3, 4, 5]}
pointers={[
{ id: "lo", index: 2, label: "lo", tone: "accent" },
{ id: "hi", index: 2, label: "hi", tone: "accent" },
{ id: "mid", index: 2, label: "mid", tone: "warning" },
]}
/>Hide the numeric index lane, compact the cells, or supply a custom accessible summary:
<PointerRow
values={values}
pointers={pointers}
showIndices={false}
compact
ariaLabel="Two-pointer scan over a sorted array."
/>Understanding the component
- Cell row. Cells are rendered left to right from index zero through the last value. Width uses a responsive
clamp()formula so the row shrinks on narrow viewports while capping atcellSizeon wider ones — the same pattern asArrayCellsandStringCells. - Pointer lane. Above the cells sits a row of one label slot per cell. Each pointer mounts inside the slot owning its index. When the index changes,
AnimatePresenceslides the old label out and the new one in. Slot ordering inside a column follows insertion order from thepointersarray. - Pointer identity. The
idfield is the stable React key. Keep it constant across renders for a given pointer — that is what lets the same "L" pointer tween from cell two to cell three instead of unmounting and remounting. - Ring colour. Each cell carries an inset ring. Cells with no pointer use the muted border tone; cells with at least one pointer use the tone of the first pointer in that cell — driven by the order of the
pointersarray, earlier pointers win the ring. - Stacking. When multiple pointers share an index, their labels stack vertically inside the column. There is no automatic deduplication — pass distinct labels or pre-combine upstream if you want a single rendered label.
- Empty row. When
valuesis empty the row collapses to the pointer lane plus the empty cells row; out-of-range pointers are dropped silently. - Reduced motion. When
prefers-reduced-motion: reduceis set, pointer transitions and cell tints snap to instant.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
values | readonly (string | number)[] | required | Cell values rendered left to right. |
pointers | readonly PointerRowPointer[] | — | Pointer overlays positioned above the cells. |
showIndices | boolean | true | Render numeric indices below each cell. |
cellSize | number | 44 | Ideal cell size in px. |
compact | boolean | false | Smaller cells, tighter row. |
ariaLabel | string | derived | Accessible summary for the row. |
transition | Transition | SPRINGS.smooth | Pointer / cell transition. |
className | string | — | Merged onto the root via cn(). |
PointerRowPointer
| Field | Type | Default | Description |
|---|---|---|---|
id | string | required | Stable React key. Must be unique within the row. |
index | number | required | Cell index the pointer sits above. Out-of-range pointers are skipped. |
label | string | required | Short label rendered above the cell. |
tone | PointerRowTone | "accent" | Tone for the label colour. |
Accessibility
- The outer container is
role="img"with anaria-labelsummarising the row — count of cells plus every in-range pointer's label and index. PassariaLabelto override the generated summary. - The pointer label lane is decorative (
aria-hidden="true"); the canonical state lives in the summary so screen readers read it once per render instead of once per label. - Cell ring colour, ring width, and the muted-vs-accent contrast layer multiple cues — the distinction stays legible for colour-blind users.
- Motion respects
prefers-reduced-motion: reduce— pointer transitions and cell tints collapse to instant.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/viz/PointerRow.tsx). The source baked the binary-search vocabulary in: hard-codedlo/mid/hiprops, avisited[]trail, asortedRangeoverlay, afoundsuccess pulse, ahiddenIndicesreveal mask, and acellStyle(idx, value)escape hatch for per-problem state. The library extract drops all of that — what remains is the geometric primitive: a row of cells with a declarative pointer list and a stable React key per pointer. Range elimination, probe rings, sorted-half tints, and visited trails all live in the caller, composed from the samepointersarray or layered as separate components on top.