Side By Side Probe
Compare a baseline against an experiment by plotting both metrics on one shared axis and then revealing the move between them. A sweep grows from the baseline marker to the experiment marker, the headline number counts up, and a bracket measures the gain — so the magnitude of a change is something you see (a distance) and the change itself is something you watch (a sweep), instead of two numbers you mentally subtract. The reveal autoplays once, then stays scrubbable and replayable.
Comparison on a shared scale. Baseline: 0.842 val acc. + LayerNorm: 0.887 val acc. Delta +0.045 val acc, an improvement.Revealed 0 percent. Current 0.842 val acc, delta 0 val acc.
Baseline+ LayerNorm
0 val acc· better0.842val acc
closed 0% of the gap to 1
Customize
Metrics
0.842
0.887
Reveal
900ms
Installation
npx shadcn@latest add https://craftbits.dev/r/side-by-side-probe.jsonUsage
import { SideBySideProbe } from "@craft-bits/core";
<SideBySideProbe
unit="val acc"
domain={[0.8, 1]}
target={1}
baseline={{ label: "Baseline", metric: 0.842 }}
experiment={{ label: "+ LayerNorm", metric: 0.887 }}
/>;A lower-is-better metric — flip higherIsBetter so a drop reads as the improvement (latency, loss, parameter count):
<SideBySideProbe
unit="ms"
higherIsBetter={false}
baseline={{ label: "Eager", metric: 84 }}
experiment={{ label: "+ KV cache", metric: 61 }}
/>Drive the reveal yourself by wiring progress to your own scrubber, scroll position, or lesson step:
const [p, setP] = useState(0);
<SideBySideProbe
progress={p}
onProgressChange={setP}
autoPlay={false}
baseline={{ label: "Before", metric: 12.4 }}
experiment={{ label: "After", metric: 18.9 }}
/>;Understanding the component
- One shared axis, two markers. Both metrics map onto a single horizontal scale (
domain), so the gap between them is a visible distance. The baseline is a hollow reference ring; the experiment is a dashed goalpost. progress ∈ [0, 1]is the whole machine. The sweep length, moving marker, counting headline, and bracket all derive from one number. Autoplay eases it0 → 1on mount; the scrubber sets it directly;progress/onProgressChangemake it fully controlled.- The sweep is the lesson. A bar grows from baseline to the live value while the headline counts up — the change is watched, not presented as a finished fact.
- The delta, measured three ways. Headline (absolute landing value), bracket (spatial span + relative
%), and badge (+0.045 · better) frame the same delta differently so significance is unambiguous. - Valence, not just sign.
higherIsBetterdecides improvement (success) vs regression (error); an arrow, the signed number, and a word all agree, so color is never the only cue. - Honest scale + headroom. Pass an explicit
domain(a tight auto-range exaggerates small gaps — the ticks show what you chose). Withtarget, the caption reframes the delta as "closed 28% of the gap to 1.00". - Reduced motion skips the sweep to the end-state; the scrubber still works.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
baseline | SideBySideProbeSide | — | Reference configuration — the fixed origin of the sweep. |
experiment | SideBySideProbeSide | — | Experiment configuration — the sweep reveals the move toward it. |
unit | string | — | Unit suffix on the headline / badge. |
domain | [number, number] | padded auto-range | Bounds of the shared axis. Pass a range for an honest scale. |
higherIsBetter | boolean | true | Whether a larger metric is the improvement. |
target | number | — | Goalpost; frames the delta as the share of the remaining gap closed. |
formatValue | (value) => string | trimmed, magnitude-aware | Format the readouts. |
formatDelta | (delta) => string | sign + formatValue(|delta|) | Format the signed delta. |
progress | number | — | Controlled reveal progress in [0, 1]. |
defaultProgress | number | 0 | Uncontrolled initial progress. |
onProgressChange | (progress) => void | — | Fires on autoplay, scrub, and replay. |
autoPlay | boolean | true | Auto-play the reveal once after mount (uncontrolled). |
revealDuration | number | 900 | Reveal-sweep duration (ms). |
transition | Transition | SPRINGS.snap | Spring for the moving marker + sweep. |
className | string | — | Merged onto the root <div> via cn(). |
Each side has the shape:
| Field | Type | Description |
|---|---|---|
label | string | Display name for the configuration. |
metric | number | Numeric value plotted on the shared axis. |
value | ReactNode | Optional override for the headline (shown when fully revealed). |
Accessibility
- The root is
role="group"labelled by a hidden sentence naming both configurations, their metrics, and the delta + valence ("an improvement" / "a regression"). - A hidden
aria-live="polite"region announces the revealed value and delta asprogresschanges — the same play-by-play as the sweep. - The scrubber is a native
<input type="range">witharia-valuetextof the current value + delta; the replay button has an explicitaria-label. Both show visible:focus-visiblerings. - Valence is triple-encoded — color, an arrow (▲ / ▼), and a word ("better" / "worse") — so direction survives red/green color-vision deficiency.
prefers-reduced-motiondisables the autoplay sweep and renders the end-state; manual scrubbing remains.
Credits
- Extracted from:
craftingattention(app/src/lessons/primitives/nn/SideBySideProbe.tsx). The source was a static two-panel layout with a floating delta chip. craft-bits re-architects it into a teaching mechanic — a shared-axis reveal that makes magnitude spatial and change temporal — while staying a general baseline-vs-experiment comparison primitive.