Boundary Markers

A row of cells with lo and hi pointer markers (and an optional mid probe) drawn on top. The alive range — cells whose index lies inclusively between the two pointers — carries an accent ring and full opacity; the eliminated halves dim to half opacity. As the caller drives the search forward, the pointer labels tween between positions with a single critically-damped spring. The component owns layout and motion; the caller owns the search.

BoundaryMarkers is the "shape" of binary search. No search logic lives here — pass length plus the current pointers on every render and the primitive draws the rest. Drop it into any binary-search narrative — lower-bound, upper-bound, "find peak", "search rotated", or any other narrowing-range algorithm.

Customize
Range
12
Display

Installation

npx shadcn@latest add https://craftbits.dev/r/boundary-markers.json

Usage

import { BoundaryMarkers } from "@craft-bits/core";
 
const length = 12;
const [lo, setLo] = useState(0);
const [hi, setHi] = useState(length - 1);
const mid = hi >= lo ? Math.floor((lo + hi) / 2) : undefined;
 
<BoundaryMarkers length={length} lo={lo} hi={hi} mid={mid} />;

Hide the mid probe — useful when the caller is showing the pointers between iterations:

<BoundaryMarkers length={12} lo={3} hi={9} />

Show array values inside the cells instead of indices:

<BoundaryMarkers
  length={8}
  lo={2}
  hi={6}
  mid={4}
  values={[1, 3, 5, 7, 9, 11, 13, 15]}
/>

Custom labels and tones — for lower / upper bound search variants where the pointers carry different meanings:

<BoundaryMarkers
  length={10}
  lo={0}
  hi={9}
  mid={4}
  loLabel="lower"
  hiLabel="upper"
  midLabel="probe"
  loTone="success"
  hiTone="error"
  midTone="warning"
/>

Understanding the component

  1. Cell row. Every cell is rendered left to right at indices 0 through length - 1. Cell width uses a responsive clamp() formula so the row shrinks on narrow viewports while capping at cellSize on wider ones — the same pattern as ArrayCells and StringCells.
  2. Alive vs. eliminated. A cell whose index lies in the inclusive range between lo and hi is alive: full opacity, accent ring. A cell outside that range is eliminated: half opacity, faint border. The transition between alive and eliminated tweens the ring colour and opacity, so eliminations read as a wash rather than a hard cut.
  3. Pointer labels. The pointer lane above the cells is a row of one label slot per cell. Labels mount inside the slot owning their pointer index; when an index changes, AnimatePresence slides the old label out and the new one in. Multiple pointers can land on the same cell (lo === hi at the end of a search), in which case the labels stack vertically.
  4. Mid emphasis. When mid is set, the mid cell gets a second accent ring in the midTone colour on top of the alive treatment — easy to spot during the probe.
  5. Empty range. When hi is less than lo, the alive range is empty and every cell renders eliminated. The accessible summary reports "alive range empty".
  6. Reduced motion. When prefers-reduced-motion: reduce is set, pointer transitions and cell tints snap to instant.

Props

PropTypeDefaultDescription
lengthnumberrequiredTotal number of cells in the range.
lonumberrequiredLower-bound marker index.
hinumberrequiredUpper-bound marker index.
midnumberOptional mid probe index.
valuesreadonly (string | number)[]Values rendered inside the cells. Defaults to the index.
showIndicesbooleantrueRender numeric indices below each cell.
loLabelstring"lo"Label for the lo pointer.
hiLabelstring"hi"Label for the hi pointer.
midLabelstring"mid"Label for the mid pointer.
loToneBoundaryMarkerTone"accent"Tone for the lo pointer.
hiToneBoundaryMarkerTone"accent"Tone for the hi pointer.
midToneBoundaryMarkerTone"warning"Tone for the mid pointer.
cellSizenumber44Ideal cell size in px.
compactbooleanfalseSmaller cells, tighter row.
transitionTransitionSPRINGS.smoothPointer / cell transition.
classNamestringMerged onto the root via cn().

Accessibility

  • The outer container is role="img" with an aria-label summarising the current state (for example, "Boundary markers: length 12, alive range 3..9, mid at 6.").
  • The pointer label lane is decorative (aria-hidden="true"); the canonical state is in the summary so screen readers read it once per render instead of three times.
  • Alive vs. eliminated cells layer ring colour + ring width + opacity, never colour alone — 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/decision/BoundaryMarkers.tsx). The original source was a 1000-line lesson teaching difference arrays through manual range updates, marker placement, and a prefix-sum sweep. The library extract drops the lesson chrome, the reducer, the audio, and the hint bar; what remains is the geometric primitive that sits underneath every binary-search-style algorithm — a range with two boundary pointers and an optional probe — generalised to a controlled length plus lo plus hi plus mid API on the Radix pattern.