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.
Installation
npx shadcn@latest add https://craftbits.dev/r/number-line.jsonUsage
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
- Axis line + auto ticks. A thin horizontal baseline runs from
paddingto100% - padding. Ticks are emitted at every multiple oftickStepinside the range. WhentickStepis omitted, the component picks5for spans over 30,2for spans over 15, and1otherwise. - 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. - Intervals are clamped.
endis clamped to be at leaststart, and both ends are clamped into the range — so it is safe to drive[start, end]from external pointer state without bounds checks. - 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. - 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. - Padding lives inside the container. All positions are computed against the inner width, so the leftmost tick sits at
paddingand the rightmost at100% - paddingregardless of container width. - Reduced motion. No JavaScript motion runs here. Adopting consumers should respect
prefers-reduced-motion: reducein 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
| Prop | Type | Default | Description |
|---|---|---|---|
range | { min: number; max: number } | required | Inclusive numeric domain rendered by the line. |
markers | NumberLineMarker[] | — | Single-value markers — pointers, tags, found-indices. |
intervals | NumberLineInterval[] | — | Closed-interval bands — windows, search domains, halves. |
padding | number | 16 | Horizontal padding inside the container, in pixels. |
tickStep | number | auto | Tick step. Auto-calculated from max - min when omitted. |
formatTick | (value: number) => string | — | Custom tick label formatter. |
height | number | 72 | Total height of the rendered region, in pixels. |
className | string | — | Merged onto the root via cn(). |
NumberLineMarker
| Field | Type | Description |
|---|---|---|
value | number | Position along the axis (in domain units). |
label | ReactNode | Optional label below the dot. Falls back to the value. |
tone | NumberLineTone | Optional tone override. Defaults to accent. |
NumberLineInterval
| Field | Type | Description |
|---|---|---|
start | number | Inclusive start in domain units. |
end | number | Inclusive end in domain units. Clamped to be at least start. |
label | ReactNode | Optional centred label above the band. |
tone | NumberLineTone | Optional tone override. Defaults to accent. |
Accessibility
- The outer container is
role="group"with a hidden summary likeNumber 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 anaria-labelofMarker at {value}; each interval isrole="img"withInterval from {start} to {end}. - Markers and intervals expose
data-toneso 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.