Array Cells
A horizontal strip of array cells with optional pointer labels above (L, R, i, READ), numeric index labels beneath, semantic per-cell tones, and three cell-state animations — pop (one-shot scale beat), pulse (ambient breathing), and dimmed (fade-out for irrelevant entries). Generic over two-pointer scans, sliding windows, BFS layers, DP tables, hash-set ledgers, or any index-keyed sequence.
Self-contained — the caller positions the strip however the parent layout wants. Cells shrink via a clamp() width so the row stays legible on narrow screens, and every colour comes from the --cb-* token palette (no hex plumbing) so the same strip repaints under a custom theme without per-call tweaks.
Installation
npx shadcn@latest add https://craftbits.dev/r/array-cells.jsonUsage
import { ArrayCells } from "@craft-bits/viz/array-cells";
<ArrayCells array={[1, 2, 3, 4, 5]} />Two-pointer scan with L / R markers:
<ArrayCells
array={[1, 2, 3, 4, 5, 6]}
pointers={[
{ index: 0, label: "L", tone: "accent" },
{ index: 5, label: "R", tone: "info" },
]}
/>Per-cell semantic tones — matched in green, rejected in red:
<ArrayCells
array={[2, 7, 11, 15]}
cellStyles={{
0: { tone: "success" },
1: { tone: "error" },
2: { tone: "success", pop: true },
}}
/>Interactive cell — every cell becomes a tappable <button> when onCellTap is set:
<ArrayCells
array={[1, 2, 3, 4]}
onCellTap={(index) => console.log("tapped", index)}
/>Understanding the component
- Generic over the array. No DP/two-pointer/sliding-window specific knobs — every algorithm encodes its state through
pointersandcellStyles. The same component covers Kadane's running max, partition-around-pivot, Boyer-Moore vote, frequency-map ticks. - Semantic tones, not hex. Every pointer and per-cell override picks a tone enum (
accent/success/warning/error/info) that resolves to the user's themed--cb-*palette. No hex plumbing, no per-lesson colour overrides. - Three cell animations.
popfires a one-shot scale beat — bounded at1.05to honour thesubtle-deformation-scaleceiling — for "this value just changed".pulseruns an ambient breathing opacity for "this cell is currently being read".dimmeddrops opacity to 0.3 for cells outside the window. - Pointer stacking. Multiple pointers can sit above the same cell — they animate in via
popLayoutsoLjoiningRon the centre cell slides the existing label aside instead of overdrawing it. - Responsive width. Cells size via
clamp(min, ideal, max)so the row shrinks under narrow viewports without dropping below the legibility floor. - Tappable vs static. When
onCellTapis set, every cell renders as a<button>withTAP_SCALEfeedback and a logical disabled state per cell. Otherwise cells render as<div role="img">with anaria-labelcovering the index and value. - Reduced motion. Pop, pulse, pointer enter, and tap-scale all collapse to instant under
prefers-reduced-motion: reduce. Text content still updates; only the motion drops.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
array | (number | string)[] | required | Array entries to render. |
pointers | ArrayCellsPointer[] | — | Pointer markers rendered above cells. Multiple pointers per cell stack horizontally. |
cellStyles | Record<number, ArrayCellsCellStyle> | — | Per-cell overrides keyed by index — tone, pop, pulse, dimmed, disabled. |
onCellTap | (index: number) => void | — | Tap handler. When set, every cell renders as a <button> with TAP_SCALE feedback. |
showIndices | boolean | true | Show numeric index labels beneath cells. |
size | "md" | "lg" | "md" | Cell size tier. Both clamp on narrow screens. |
animateLayout | boolean | false | Enable per-cell layout animation for smooth re-orders. |
layoutNamespace | string | — | Shared-element layoutId prefix. |
transition | Transition | SPRINGS.snap | Override the cell pop / pointer-swap transition. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The outer
<div>isrole="group"with anaria-labelledbyhook into a screen-reader-only summary ("Array, 8 cells, 2 pointers."). - Each cell carries
aria-label="Index {i}: {value}"so tabbing through tappable cells (or focusing a static row with VoiceOver) yields the same readout as scanning the visible text. - Pointer labels are
aria-hidden; the cell tone they drive is already announced via the cell'sdata-toneattribute. - Tones never carry the only signal — every state also shifts the cell's fill, stroke, and ink simultaneously, so colourblind readers still pick up the success / warning / error transition.
- Motion respects
prefers-reduced-motion: reduce— pop, pulse, pointer enter, and tap-scale all collapse to instant.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/viz/ArrayCells.tsx). The source expected rawcolorhex strings on everyPointerDefplus a 9-fieldCellStyle(bgHex+bgAlpha+ringHex+textHex+textClass+pulse+pop+dimmed+disabled) and bound text colour to lesson-specific--color-ink-*CSS variables that don't exist outside the lesson runtime. The craft-bits version replaces every hex with the six-toneArrayCellsToneenum, collapses the per-cell override schema to five semantic flags, swaps theSPRINGS.snappyreference for the canonicalSPRINGS.snap, and re-points every colour at the--cb-*token system.