Status Badge
A badge with a count that rolls digit by digit, a removable mode that shrinks out while neighbours slide into place, and a pulsing variant for live states.
A badge for tags, states and unread counts. When count changes, each digit rolls to its new value on a spring; added digits grow in from the left, removed ones collapse, and anything above maxCount shows as "99+". When a count first appears, the whole count bubble pops in. With removable, a × appears on the right: clicking it shrinks, fades and collapses the badge, and onRemove fires only once the animation ends. Inside StatusBadge.Group, neighbours slide into place with a FLIP. pulse adds a leading dot with an expanding ring, ideal for "LIVE" style states.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add status-badge.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<StatusBadge/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
Installation
npx webberui@latest add https://webberui.com/r/status-badge.en.jsonOr, once registries are configured in components.json, install it as @webberui/status-badge.
Usage
import { StatusBadge } from "@/components/ui/status-badge";
<StatusBadge count={12} countLabel={(n) => `${n} unread`}>
Inbox
</StatusBadge>
<StatusBadge variant="destructive" pulse>
LIVE
</StatusBadge>A removable tag row: put the badges inside StatusBadge.Group (an AnimatePresence + flex container), keep the list in the parent, and drop the item in onRemove.
const [tags, setTags] = React.useState(["Design systems", "Accessibility", "Performance"]);
<StatusBadge.Group label="Post tags">
{tags.map((tag) => (
<StatusBadge
key={tag}
variant="outline"
removable
onRemove={() => setTags((prev) => prev.filter((t) => t !== tag))}
>
{tag}
</StatusBadge>
))}
</StatusBadge.Group>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "outline" | "destructive" | "success" | "default" | Visual variant |
children | ReactNode | — | Badge text |
count | number | — | Count; shown to the right of the text (or on its own without text), rolling digit by digit when it changes |
maxCount | number | 99 | Count cap; above it the badge shows "cap+" (e.g. 99+) |
showZero | boolean | false | Whether to still show the count when count is 0 |
icon | ReactNode | — | Leading icon |
pulse | boolean | false | Shows a leading dot with an endlessly expanding pulse (live, connecting and similar states) |
removable | boolean | false | Shows a remove button; clicking it shrinks the badge out, then calls onRemove |
onRemove | () => void | — | Called after the exit animation finishes; the parent removes this badge from its list |
removeLabel | string | "Remove " + text | aria-label for the remove button |
size | "sm" | "md" | "md" | Size |
stiffness | number | 300 | Stiffness of the digit-rolling spring |
damping | number | 30 | Damping of the digit-rolling spring |
countLabel | (count: number) => string | String(count) | Screen-reader announcement when the count changes |
ref | Ref<HTMLSpanElement> | — | Ref forwarded to the root element |
className | string | — | Passed through to the root element |
StatusBadge.Group
| Field | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | A set of StatusBadges; each needs a stable key, and the parent owns the list |
label | string | — | aria-label for the group; when set the container gets role="group" |
className | string | — | Passed through to the group container (defaults to flex flex-wrap gap-2) |
How it works
- Digit rolling: each digit is an
overflow-hiddencolumn holding a vertical 0–9 strip, and a spring rolls itsyto the target; the ones digit moves first and higher digits follow. Numbers always usetabular-nums, so the width does not jitter. - Removal: clicking × exits the badge with
scale→0,opacity→0and a collapsing width, and only then callsonRemove. The badge carrieslayout="position", so inside a flex container its neighbours FLIP into place on add and remove (position only — text is never squashed). - Rolling your own container works too: put the badges in
<AnimatePresence mode="popLayout">plus arelative flexcontainer and manage the list in the parent;StatusBadge.Groupis exactly that combination. - Timing variables: the exit lasts 150ms by default and can be overridden with
--wb-status-badge-duration(e.g.200ms); the pulse dot follows the text colour by default and can be overridden with--wb-status-badge-pulse-color.
Accessibility
- The count animation is hidden from assistive tech; an sr-only
aria-live="polite"text announces it instead, produced bycountLabel(e.g. "12 unread") - The pulse dot and the icon are
aria-hidden - × is a
type="button"whosearia-labeldefaults to "Remove" plus the badge text; when focused, Enter, Space, Delete or Backspace removes it, with a focus-visible ring StatusBadge.Groupgetsrole="group"and anaria-labelwhenlabelis set- When the user's system has "reduce motion" on, digits swap instantly, the pulse stops expanding and leaves a static dot, removal only fades (no scaling) and neighbours jump straight into place
Sortable Data Table
General-purpose data table with FLIP row reordering on header sort, a sticky header, expandable row details, checkbox selection with a bulk-action bar, and an empty state
Avatar
An avatar that shows initials until the image loads and then crossfades, with a breathing presence dot, a hover ring that expands, five sizes and two shapes.