Count Badge
One badge, three jobs. In count mode it shows a number and swaps digits with a
short vertical roll, capping at max with a trailing plus. In status mode it
becomes a presence dot whose colour carries online, away, busy or offline. In
dot mode it is a bare marker, optionally with a ping ring.
All three share the same anchor logic: the badge is absolutely positioned against a relative wrapper, so it hugs whichever corner you choose without affecting the child's layout. The count only mounts when it has something to say, which keeps an empty inbox free of a stray zero.
Usage
Wrap the thing being annotated and pick a variant. Counts above max render as
99+ by default; showZero keeps a zero visible when the difference between
"none" and "not loaded" matters.
<template>
<div class="flex flex-wrap items-center justify-center gap-8 py-8">
<MCountBadge
:count="8"
variant="count"
>
<span class="grid size-12 place-items-center rounded-full bg-elevated ring-1 ring-default">
<UIcon
class="size-5 text-muted"
name="i-lucide-bell"
/>
</span>
</MCountBadge>
<MCountBadge
:count="128"
:max="99"
variant="count"
>
<span class="grid size-12 place-items-center rounded-full bg-elevated ring-1 ring-default">
<UIcon
class="size-5 text-muted"
name="i-lucide-inbox"
/>
</span>
</MCountBadge>
<MCountBadge
status="online"
variant="status"
>
<span class="grid size-12 place-items-center rounded-full bg-elevated ring-1 ring-default">
<UIcon
class="size-5 text-muted"
name="i-lucide-user"
/>
</span>
</MCountBadge>
<MCountBadge
ping
variant="dot"
>
<span class="grid size-12 place-items-center rounded-full bg-elevated ring-1 ring-default">
<UIcon
class="size-5 text-muted"
name="i-lucide-radio"
/>
</span>
</MCountBadge>
</div>
</template>
With no slot the badge renders beside a small spacer, which is handy when the anchor lives elsewhere in the layout.
Customize
Customize
Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'dot' | 'count' | 'status' | 'dot' | What the badge shows. |
count | number | 0 | Value displayed in the count variant. |
max | number | 99 | Value at which the badge switches to max+. |
status | 'online' | 'offline' | 'busy' | 'away' | 'online' | Presence state used by the status variant. |
position | 'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-right' | Corner the badge is anchored to. |
showZero | boolean | false | Show the count badge when the value is zero. |
ping | boolean | false | Render an extra ping ring behind the badge. |
class | string | - | Extra classes for the badge itself. |
Slots
| Slot | Props | Description |
|---|---|---|
default | - | The element the badge annotates. It keeps its own layout and events. |
Accessibility
- The
dotvariant isaria-hidden; it is a decorative marker with no text equivalent. - The
statusvariant carries visually hidden text with the status name, so presence is never conveyed by colour alone. - The
countvariant renders the number as real text, which screen readers announce in place. - The ping ring is
aria-hiddenand stops underprefers-reduced-motion. - The badge itself is not focusable and never steals a click from the child.
Performance
- The badge mounts only when it is visible; an empty count removes the node instead of hiding it.
- A changing count swaps one keyed span inside an overflow-hidden box, so only the digits animate.
- The pop-in and digit roll are CSS animations; under
prefers-reduced-motionthey switch off and the number updates instantly. - The ping is a CSS animation, and no JavaScript timers run in the component.
- Position and size come from static classes, so there is no measurement in the render path.