Counter Badge
A compact counter for live algorithm and dashboard chrome. Three layers
stacked right-aligned: a small uppercase label, a large number that swaps in
via an AnimatePresence digit-swap on every change, and a proportional
progress bar. When the value reaches maxValue, the badge flips to the
success tone — a visual "peak" marker independent of the base tone.
Active
3
Rooms
3
Score
3
Installation
npx shadcn@latest add https://craftbits.dev/r/counter-badge.jsonUsage
import { CounterBadge } from "@craft-bits/core";
<CounterBadge value={3} maxValue={8} label="Active" />Pass a tone for the non-peak baseline; the badge still flips to success
when it peaks:
<CounterBadge value={7} maxValue={8} label="Rooms" tone="info" />Drop the trailing track for a pure label-plus-number readout:
<CounterBadge value={1234} label="Score" hideTrack />Understanding the component
- Digit swap on value change. The number is keyed by
valueinside anAnimatePresencewithmode="popLayout". Each new value enters from the top and the old digit exits downward viaSPRINGS.snap, so changes are felt as motion.initial={false}suppresses the first-render animation. - Progress bar via transform, not width. The fill is a
100%-wide block scaled viascaleX = value / maxValueand animated withSPRINGS.smooth. Animatingtransformkeeps the bar GPU-composited; animatingwidthwould force layout each frame. - Tone + peak. Six base tones drive the number color and fill. When
maxValue > 0andvalue >= maxValue, a compound CVA variant overrides both to the success tone — the badge peaks green regardless of base tone. - Three sizes.
sm,md(default),lgscale label, number, and track together so proportions hold from inline-row to hero readouts. - Live region. The number container is
aria-live="polite"witharia-atomic="true". The progress bar isaria-hidden— it visually duplicates the same number assistive tech already hears.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | required | Current counter value. Each change drives the digit-swap. |
maxValue | number | 0 | Peak used to scale the progress bar. When value >= maxValue > 0, the badge swaps to the success tone. |
label | ReactNode | — | Optional label rendered above the number. |
tone | 'default' | 'success' | 'warning' | 'error' | 'info' | 'accent' | 'default' | Base color of the number and fill (overridden when at peak). |
size | 'sm' | 'md' | 'lg' | 'md' | Visual size — drives number, label, and track scale. |
hideTrack | boolean | false | When true, the trailing progress bar is omitted. |
className | string | — | Merged onto the rendered root <div>. |
Accessibility
- The number container carries
aria-live="polite"andaria-atomic="true", so screen readers announce the new value as a single utterance on change. - The progress bar is
aria-hidden="true"— it visually duplicates the same number assistive tech already hears via the live region. - The digit-swap animation uses transform + opacity only; the fill bar
animates
scaleX. Both respectprefers-reduced-motionvia the underlying Motion preset (SPRINGS.snap,SPRINGS.smooth). - Color contrast for every tone meets WCAG AA against
--cb-bgin both themes; verify with custom theme tokens if you re-skin.
Credits
- Extracted from:
algoflashcards(src/lessons/primitives/chrome/CounterBadge.tsx). Generalized from a sweep-line overlap counter into a tone-driven, size-tiered library primitive.