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.json

Usage

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

  1. Digit swap on value change. The number is keyed by value inside an AnimatePresence with mode="popLayout". Each new value enters from the top and the old digit exits downward via SPRINGS.snap, so changes are felt as motion. initial={false} suppresses the first-render animation.
  2. Progress bar via transform, not width. The fill is a 100%-wide block scaled via scaleX = value / maxValue and animated with SPRINGS.smooth. Animating transform keeps the bar GPU-composited; animating width would force layout each frame.
  3. Tone + peak. Six base tones drive the number color and fill. When maxValue > 0 and value >= maxValue, a compound CVA variant overrides both to the success tone — the badge peaks green regardless of base tone.
  4. Three sizes. sm, md (default), lg scale label, number, and track together so proportions hold from inline-row to hero readouts.
  5. Live region. The number container is aria-live="polite" with aria-atomic="true". The progress bar is aria-hidden — it visually duplicates the same number assistive tech already hears.

Props

PropTypeDefaultDescription
valuenumberrequiredCurrent counter value. Each change drives the digit-swap.
maxValuenumber0Peak used to scale the progress bar. When value >= maxValue > 0, the badge swaps to the success tone.
labelReactNodeOptional 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.
hideTrackbooleanfalseWhen true, the trailing progress bar is omitted.
classNamestringMerged onto the rendered root <div>.

Accessibility

  • The number container carries aria-live="polite" and aria-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 respect prefers-reduced-motion via the underlying Motion preset (SPRINGS.snap, SPRINGS.smooth).
  • Color contrast for every tone meets WCAG AA against --cb-bg in 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.