Count Badge
Numeric overlay for unread and pending counts on an icon or avatar — overflow suffix, zero handling, and an optional polite live region.
Count Badge is a numeric overlay for unread and pending counts — on a bell, an avatar, or standing alone. It is a count, not a status. Use Status Tag for lifecycle and Label Tag for categories. Full class map: Messaging Surfaces.
When To Use
- Overlay an unread or pending count on an icon button or avatar (shell bell, sidebar entry, avatar with pending work).
- Show a standalone numeric chip when there is no host control to wrap.
- Use
dotwhen you only need “there is something,” not the number. - Pair with Notification Center for the shell bell — the button owns the accessible name, and the badge stays decorative.
When Not To Use
- Do not fake a count with Status Tag, Label Tag, or shadcn
Badgetext. Count Badge is the only numeric overlay in this kit. - Do not use it for status, severity, or category — those are tags, not counts. See Tag Selection.
- Do not put a
labelon the badge when it sits inside a named control. The host control owns the accessible name (Notifications, 3 new); a second live region double-announces. Details: Messaging Surfaces. - Do not use it as a notification list, toast, or object-message trigger. Pick the class first on Messaging Surfaces.
Features
| Area | Behavior |
|---|---|
| Display | Integer count; values above overflowCount (default 99) render as 99+ |
| Visibility | Hidden at 0 unless showZero. dot follows the same gate and never shows digits |
| Layout | No children → standalone chip. With children → absolute overlay on the host control (not the inner SVG) |
| Tone | attention (destructive) or neutral — no success / warning / info. This is a count, not a status |
A11y without label | Visual badge is aria-hidden. The host control must name the count |
A11y with label | Persistent polite live region (role="status") even while the badge is hidden at 0. Use only when there is no host control to name |
| Clamping | Negatives clamp to 0 (hidden unless showZero). Fractional values floor (3.7 → 3) |
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/count-badge.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/count-badge.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/count-badge.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/count-badge.jsonWith a namespace: npx shadcn@latest add @f-ui/count-badge.
registryDependencies: utils.
Usage
import { CountBadge } from "@/components/f-ui/count-badge/count-badge";
import { Button } from "@/components/ui/button";
import { Bell } from "lucide-react";
// Overlay — wrap the host control, not the 16px glyph (Ant Badge)
<CountBadge count={3}>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Notifications, 3 new"
>
<Bell className="size-4" aria-hidden />
</Button>
</CountBadge>
// Standalone with a live region (no host control to name)
<CountBadge count={3} label="unread notifications" />Examples
Standalone, Overflow, Zero, and Dot
Standalone chips: a count, overflow (99+), hidden zero vs showZero, a digitless dot, and both tones. The badge is a count — attention or neutral only.
Standalone
Overflow
Zero (hidden)
showZero
Dot
Dot at zero
Dot with showZero
Attention
Neutral
"use client";
import type { ReactNode } from "react";
import { CountBadge } from "@/components/f-ui/count-badge/count-badge";
function Sample({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<div className="flex flex-col items-start gap-2">
<p className="text-sm font-medium text-muted-foreground">{label}</p>
{children}
</div>
);
}
export function CountBadgeDemo() {
return (
<div className="flex flex-col gap-8">
<div className="flex flex-wrap items-end gap-8">
<Sample label="Standalone">
<CountBadge count={5} />
</Sample>
<Sample label="Overflow">
<CountBadge count={150} />
</Sample>
<Sample label="Zero (hidden)">
<CountBadge count={0} />
</Sample>
<Sample label="showZero">
<CountBadge count={0} showZero />
</Sample>
</div>
<div className="flex flex-wrap items-end gap-8">
<Sample label="Dot">
<CountBadge count={5} dot />
</Sample>
<Sample label="Dot at zero">
<CountBadge count={0} dot />
</Sample>
<Sample label="Dot with showZero">
<CountBadge count={0} dot showZero />
</Sample>
</div>
<div className="flex flex-wrap items-end gap-8">
<Sample label="Attention">
<CountBadge count={5} tone="attention" />
</Sample>
<Sample label="Neutral">
<CountBadge count={5} tone="neutral" />
</Sample>
</div>
</div>
);
}Overlay on a Control
Wrap the host control (the icon button or avatar), not the inner SVG. The host carries aria-label="Notifications, 3 new" (or an equivalent name). The nested Count Badge has no label, so it stays decorative and does not announce twice.
Icon button
Avatar
JD"use client";
import { Bell } from "lucide-react";
import { CountBadge } from "@/components/f-ui/count-badge/count-badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export function CountBadgeOverlayDemo() {
return (
<TooltipProvider>
<div className="flex flex-wrap items-end gap-10">
<div className="flex flex-col items-start gap-2">
<p className="text-sm font-medium text-muted-foreground">
Icon button
</p>
<CountBadge count={3}>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="ghost"
size="icon"
aria-label="Notifications, 3 new"
>
<Bell className="size-4" aria-hidden />
</Button>
</TooltipTrigger>
<TooltipContent>Notifications</TooltipContent>
</Tooltip>
</CountBadge>
</div>
<div className="flex flex-col items-start gap-2">
<p className="text-sm font-medium text-muted-foreground">Avatar</p>
<CountBadge count={3}>
<Avatar role="img" aria-label="Jordan Doe, 3 unread">
<AvatarFallback>JD</AvatarFallback>
</Avatar>
</CountBadge>
</div>
</div>
</TooltipProvider>
);
}API Reference
Props
CountBadge
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 0 | Value to display. Floored to an integer; negatives clamp to 0 |
overflowCount | number | 99 | Max before the + suffix. 150 with the default renders 99+ |
showZero | boolean | false | Render when the value is 0. Applies to both the number and dot |
dot | boolean | false | Marker instead of digits. Still hidden at 0 unless showZero |
tone | "attention" | "neutral" | "attention" | Count chrome only. Not a status family |
label | string | — | sr-only unit phrase (e.g. "unread notifications"). When set, the badge owns a persistent polite live region. Omit when the host control already names the count |
children | ReactNode | — | Overlay target. Standalone chip when omitted |
className | string | — | Overlay: the relative wrapper. Standalone: the chip |
CountBadgeTone is "attention" | "neutral".