Comparison Counter
A small labelled counter built for algorithm visualisers. Each time the parent updates value, the number re-mounts and runs a bouncy scale animation, so a live operation count reads as motion rather than as a silent number swap.
Preview
Comparisons:0
Customize
Options
700
1.15
Installation
npx shadcn@latest add https://craftbits.dev/r/comparison-counter.jsonUsage
import { ComparisonCounter } from "@craft-bits/edu";
<ComparisonCounter value={count} label="Comparisons:" />Tint the value by setting color on a parent. The value uses currentColor, so any wrapper that sets text-cb-accent (or any color token) flows through:
<div className="text-cb-accent">
<ComparisonCounter value={count} />
</div>Fire a callback once the counter first starts incrementing — useful for kicking off a follow-up animation when the algorithm finishes its first step:
<ComparisonCounter
value={count}
onComplete={(v) => console.log("first tick", v)}
/>Anatomy
- Root —
inline-flexrow, spreads unknown props onto a<div>withrole="status"andaria-live="polite". - Label — muted monospace text on the left. Pass an empty string to suppress it.
- Value — tabular-nums, coloured by
currentColor, wrapped inAnimatePresencewithmode="popLayout"and keyed by the value itself. - Pop on change — every value update animates
scalefrommaxScaleto1onSPRINGS.bouncy. ThemaxScaleprop is clamped to[1, 1.2]by thecraft-bits/subtle-deformation-scalelint rule. - Reduced motion — when the user prefers reduced motion, the entrance scale is skipped — the new digit appears immediately at scale
1. - onComplete — fires at most once per instance, on the first
valueupdate after mount.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | required | Numeric value to display. Drives the pop-on-change animation. |
label | ReactNode | 'Comparisons:' | Label to the left of the value. Empty string suppresses it. |
maxScale | number | 1.15 | Peak scale during the value-change animation. Clamped to [1, 1.2]. |
onComplete | (value) => void | — | Fires once when value first changes after mount. |
aria-label | string | derived | Accessible label for the status region. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The root is
role="status"witharia-live="polite"andaria-atomic="true", so screen readers announce the new value on every change. - When
labelis a string, thearia-labelfalls back to"<label> <value>"for screen-reader context. Override it via thearia-labelprop for non-string labels. - Reduced-motion users get an instant value swap — no scale animation.
Credits
- Extracted from:
AlgoFlashcards(src/lessons/primitives/chrome/ComparisonCounter.tsx). The original used an inlinestylecolor and a project-specific spring import; this primitive consumescurrentColor, the sharedSPRINGS.bouncytoken, and adds anonCompletehook for terminal-state wiring.