NumberLine

A horizontal axis with auto-spaced ticks, optional single-value markers (pointers, tags, hits) and optional intervals (closed ranges painted as bands across the axis). Generic over the underlying use-case: timelines, search domains, sliding windows, two-pointer scans, binary-search boundaries, or any axis-keyed visualisation.

Per-marker and per-interval tones let you colour-code algorithm state — frontier vs settled vs discarded — without bespoke styling. Both markers and intervals layer over the same axis, so a single component covers an [L, R] window with a mid pointer, a [low, high] search domain with the found index, or any other axis-tag combination.

Number line from 0 to 20, 3 markers, 1 interval.
window

Installation

npx shadcn@latest add https://craftbits.dev/r/number-line.json

Usage

import { NumberLine } from "@craft-bits/core";
 
<NumberLine
  range={{ min: 0, max: 20 }}
  markers={[
    { value: 4, label: "L", tone: "success" },
    { value: 9, label: "R", tone: "success" },
    { value: 6, label: "mid", tone: "warning" },
  ]}
  intervals={[{ start: 4, end: 9, label: "window" }]}
/>

Markers alone — render a sparse set of pointers on a long axis:

<NumberLine
  range={{ min: 0, max: 100 }}
  markers={[
    { value: 23, label: "found" },
    { value: 64, label: "next" },
  ]}
/>

Intervals alone — paint partition halves without any pointer dots:

<NumberLine
  range={{ min: 0, max: 15 }}
  intervals={[
    { start: 0, end: 7, label: "left", tone: "success" },
    { start: 8, end: 15, label: "right", tone: "error" },
  ]}
/>

Custom tick formatter — useful for time axes or prefixed labels:

<NumberLine
  range={{ min: 0, max: 10 }}
  tickStep={1}
  formatTick={(t) => "t+" + t}
/>

Understanding the component

  1. Axis line + auto ticks. A thin horizontal baseline runs from padding to 100% - padding. Ticks are emitted at every multiple of tickStep inside the range. When tickStep is omitted, the component picks 5 for spans over 30, 2 for spans over 15, and 1 otherwise.
  2. Markers stack a dot and a label. Each marker renders a 12 px dot at its value, with the label below the axis. Out-of-range markers (outside [range.min, range.max]) are silently skipped.
  3. Intervals are clamped. end is clamped to be at least start, and both ends are clamped into the range — so it is safe to drive [start, end] from external pointer state without bounds checks.
  4. Tone palette. Both markers and intervals accept the same tone: accent (default), success, warning, error, muted. The tone drives the dot fill, the band fill (at 14% alpha) and ring (55% alpha), and the label colour.
  5. No motion of its own. The component is intentionally render-only — caller-driven props are the animation. Pair with the consumer's own transition primitive (AnimatePresence, motion.div) when the markers need to slide.
  6. Padding lives inside the container. All positions are computed against the inner width, so the leftmost tick sits at padding and the rightmost at 100% - padding regardless of container width.
  7. Reduced motion. No JavaScript motion runs here. Adopting consumers should respect prefers-reduced-motion: reduce in any pointer-slide animation they wire on top.

Variants

Just an axis — no markers, no intervals:

<NumberLine range={{ min: 0, max: 50 }} />

A two-pointer scan with a settled prefix:

<NumberLine
  range={{ min: 0, max: 12 }}
  intervals={[{ start: 0, end: 3, tone: "muted", label: "scanned" }]}
  markers={[
    { value: 4, label: "L", tone: "success" },
    { value: 7, label: "R", tone: "success" },
  ]}
/>

A binary-search step with eliminated halves:

<NumberLine
  range={{ min: 0, max: 15 }}
  intervals={[
    { start: 0, end: 6, tone: "error", label: "out" },
    { start: 8, end: 15, tone: "muted", label: "next" },
  ]}
  markers={[{ value: 7, label: "mid", tone: "warning" }]}
/>

Props

PropTypeDefaultDescription
range{ min: number; max: number }requiredInclusive numeric domain rendered by the line.
markersNumberLineMarker[]Single-value markers — pointers, tags, found-indices.
intervalsNumberLineInterval[]Closed-interval bands — windows, search domains, halves.
paddingnumber16Horizontal padding inside the container, in pixels.
tickStepnumberautoTick step. Auto-calculated from max - min when omitted.
formatTick(value: number) => stringCustom tick label formatter.
heightnumber72Total height of the rendered region, in pixels.
classNamestringMerged onto the root via cn().

NumberLineMarker

FieldTypeDescription
valuenumberPosition along the axis (in domain units).
labelReactNodeOptional label below the dot. Falls back to the value.
toneNumberLineToneOptional tone override. Defaults to accent.

NumberLineInterval

FieldTypeDescription
startnumberInclusive start in domain units.
endnumberInclusive end in domain units. Clamped to be at least start.
labelReactNodeOptional centred label above the band.
toneNumberLineToneOptional tone override. Defaults to accent.

Accessibility

  • The outer container is role="group" with a hidden summary like Number line from 0 to 20, 2 markers, 1 interval. so screen readers hear the line's state at a glance.
  • Each marker is role="img" with an aria-label of Marker at {value}; each interval is role="img" with Interval from {start} to {end}.
  • Markers and intervals expose data-tone so consumer apps can hook custom styles or assistive tooling.
  • Colour is never the only signal — markers carry a label, intervals carry an optional label, and ticks render numeric values alongside the bands.
  • The component does not own motion. Consumer animations should respect prefers-reduced-motion: reduce.

Credits

  • Extracted from: algoflashcards (src/lessons/primitives/viz/NumberLine.tsx). The source was a positioning-only utility paired with two helper functions (toPercent, leftCalc) and rendered a bare axis with tick labels — markers and intervals lived in callers. The library extract folds the marker and interval primitives into the component so the same axis covers timelines, sliding windows, two-pointer scans, and binary-search boundaries; both helpers stay exported for consumers that want to position custom overlays on the same coordinate system.