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.

Array, 8 cells, 2 pointers.
Customize
Shape
8
md
Static markers
0
4
Playback
600 ms

Installation

npx shadcn@latest add https://craftbits.dev/r/array-cells.json

Usage

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

  1. Generic over the array. No DP/two-pointer/sliding-window specific knobs — every algorithm encodes its state through pointers and cellStyles. The same component covers Kadane's running max, partition-around-pivot, Boyer-Moore vote, frequency-map ticks.
  2. 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.
  3. Three cell animations. pop fires a one-shot scale beat — bounded at 1.05 to honour the subtle-deformation-scale ceiling — for "this value just changed". pulse runs an ambient breathing opacity for "this cell is currently being read". dimmed drops opacity to 0.3 for cells outside the window.
  4. Pointer stacking. Multiple pointers can sit above the same cell — they animate in via popLayout so L joining R on the centre cell slides the existing label aside instead of overdrawing it.
  5. Responsive width. Cells size via clamp(min, ideal, max) so the row shrinks under narrow viewports without dropping below the legibility floor.
  6. Tappable vs static. When onCellTap is set, every cell renders as a <button> with TAP_SCALE feedback and a logical disabled state per cell. Otherwise cells render as <div role="img"> with an aria-label covering the index and value.
  7. 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

PropTypeDefaultDescription
array(number | string)[]requiredArray entries to render.
pointersArrayCellsPointer[]Pointer markers rendered above cells. Multiple pointers per cell stack horizontally.
cellStylesRecord<number, ArrayCellsCellStyle>Per-cell overrides keyed by index — tone, pop, pulse, dimmed, disabled.
onCellTap(index: number) => voidTap handler. When set, every cell renders as a <button> with TAP_SCALE feedback.
showIndicesbooleantrueShow numeric index labels beneath cells.
size"md" | "lg""md"Cell size tier. Both clamp on narrow screens.
animateLayoutbooleanfalseEnable per-cell layout animation for smooth re-orders.
layoutNamespacestringShared-element layoutId prefix.
transitionTransitionSPRINGS.snapOverride the cell pop / pointer-swap transition.
classNamestringMerged onto the root via cn().

Accessibility

  • The outer <div> is role="group" with an aria-labelledby hook 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's data-tone attribute.
  • 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 raw color hex strings on every PointerDef plus a 9-field CellStyle (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-tone ArrayCellsTone enum, collapses the per-cell override schema to five semantic flags, swaps the SPRINGS.snappy reference for the canonical SPRINGS.snap, and re-points every colour at the --cb-* token system.