Data Display

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

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

12
300
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
variant"default" | "secondary" | "outline" | "destructive" | "success""default"Visual variant
childrenReactNode—Badge text
countnumber—Count; shown to the right of the text (or on its own without text), rolling digit by digit when it changes
maxCountnumber99Count cap; above it the badge shows "cap+" (e.g. 99+)
showZerobooleanfalseWhether to still show the count when count is 0
iconReactNode—Leading icon
pulsebooleanfalseShows a leading dot with an endlessly expanding pulse (live, connecting and similar states)
removablebooleanfalseShows 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
removeLabelstring"Remove " + textaria-label for the remove button
size"sm" | "md""md"Size
stiffnessnumber300Stiffness of the digit-rolling spring
dampingnumber30Damping of the digit-rolling spring
countLabel(count: number) => stringString(count)Screen-reader announcement when the count changes
refRef<HTMLSpanElement>—Ref forwarded to the root element
classNamestring—Passed through to the root element

StatusBadge.Group

FieldTypeDefaultDescription
childrenReactNode—A set of StatusBadges; each needs a stable key, and the parent owns the list
labelstring—aria-label for the group; when set the container gets role="group"
classNamestring—Passed through to the group container (defaults to flex flex-wrap gap-2)

How it works

  • Digit rolling: each digit is an overflow-hidden column holding a vertical 0–9 strip, and a spring rolls its y to the target; the ones digit moves first and higher digits follow. Numbers always use tabular-nums, so the width does not jitter.
  • Removal: clicking × exits the badge with scale→0, opacity→0 and a collapsing width, and only then calls onRemove. The badge carries layout="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 a relative flex container and manage the list in the parent; StatusBadge.Group is 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 by countLabel (e.g. "12 unread")
  • The pulse dot and the icon are aria-hidden
  • × is a type="button" whose aria-label defaults to "Remove" plus the badge text; when focused, Enter, Space, Delete or Backspace removes it, with a focus-visible ring
  • StatusBadge.Group gets role="group" and an aria-label when label is 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