f-ui
Components

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 dot when 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 Badge text. 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 label on 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

AreaBehavior
DisplayInteger count; values above overflowCount (default 99) render as 99+
VisibilityHidden at 0 unless showZero. dot follows the same gate and never shows digits
LayoutNo children → standalone chip. With children → absolute overlay on the host control (not the inner SVG)
Toneattention (destructive) or neutral — no success / warning / info. This is a count, not a status
A11y without labelVisual badge is aria-hidden. The host control must name the count
A11y with labelPersistent polite live region (role="status") even while the badge is hidden at 0. Use only when there is no host control to name
ClampingNegatives clamp to 0 (hidden unless showZero). Fractional values floor (3.73)

Installing

pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/count-badge.json
npx shadcn@latest add https://ui.isaacfei.com/r/count-badge.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/count-badge.json
bun x shadcn@latest add https://ui.isaacfei.com/r/count-badge.json

With 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

PropTypeDefaultDescription
countnumber0Value to display. Floored to an integer; negatives clamp to 0
overflowCountnumber99Max before the + suffix. 150 with the default renders 99+
showZerobooleanfalseRender when the value is 0. Applies to both the number and dot
dotbooleanfalseMarker instead of digits. Still hidden at 0 unless showZero
tone"attention" | "neutral""attention"Count chrome only. Not a status family
labelstringsr-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
childrenReactNodeOverlay target. Standalone chip when omitted
classNamestringOverlay: the relative wrapper. Standalone: the chip

CountBadgeTone is "attention" | "neutral".

On this page