Badge React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React badge for status chips, removable filters and animated counts in three variants and six tones, plus a count or dot on any corner.
A status badge is plain text in a tinted pill; with pulse its dot pings like a live light. A count rolls to the new number, up when it grows and down when it shrinks, the pill widens on a smooth spring and gives a small bounce on every new item; at 0 it scales away. Removable chips show a round x: click or tap it (a 36px target on touch), or focus it with Tab and press Enter, Backspace or Delete. With asChild the badge is a real link or button with a hover tint, a press dip, a focus ring and a 44px tall touch target. BadgeAnchor pins a badge to a corner of an icon button or avatar, cut out by a thin ring, and pops it in and out.
Badge is a free React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add badge, together with Motion presets, Tones and UIProvider and the npm packages @radix-ui/react-slot, lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add badgeUsage
Import Badge from @/components/ui/badge.
import { Bell } from "lucide-react"
import { Badge, BadgeAnchor } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
export function OrderHeader({ unread, onOpenInbox }: { unread: number; onOpenInbox: () => void }) {
return (
<div className="flex items-center gap-3">
<Badge tone="success" dot>Paid</Badge>
<Badge tone="warning">Low stock</Badge>
<BadgeAnchor offset={5} badge={<Badge variant="solid" tone="danger" size="sm" count={unread} aria-hidden />}>
<Button variant="soft" size="icon" radius="full" aria-label={`Notifications, ${unread} unread`} onClick={onOpenInbox}>
<Bell />
</Button>
</BadgeAnchor>
</div>
)
}Props
childrendefault"Nou"ReactNodeThe label. Long labels truncate with an ellipsis. Leave it empty (with no icons or count) and the badge becomes a status dot.
variantdefault"soft""solid" | "soft" | "outline"soft (a tinted pill) for statuses, solid for counts and the one label that must stand out, outline for filters and quiet tags.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Semantic color from the design tokens: success for paid or online, warning for low stock, danger for cancelled or failed, info for in progress. Pair color with the text, never color alone.
colordefault""stringAny CSS color. Replaces the tone in every variant; text on a solid fill turns black or white by lightness. Leave empty to use the tone.
sizedefault"md""sm" | "md" | "lg"Height 20 / 24 / 28px with 11 / 12 / 13px text and 12 / 12 / 14px icons.
radiusdefault"full""none" | "sm" | "md" | "lg" | "full"Corner shape: 0 / 4 / 6 / 8px or a pill. Badges default to pills (a deliberate difference from controls); the square steps suit tag-like labels.
dotdefaultfalsebooleanA leading status dot in the tone (in the text color on a solid badge).
pulsedefaultfalsebooleanThe dot pings, for live, recording or online. Implies dot. Decorative (hidden from screen readers); under reduced motion the dot stays still.
maxdefault99numberCounts above it read "99+". Values below 1 fall back to 99. Only used with count.
showZerodefaultfalsebooleanA count of 0 scales the badge away unless this is set. Try it on the bell: tap it to mark all as read.
localedefault""stringFormats the count through Intl, e.g. "en-US" turns 1250 into 1,250 and "de-DE" into 1.250. Empty uses the reader's locale; pass one for server-rendered counts of 1000 or more so the grouping matches after hydration.
removeLabeldefault"Remove"stringaria-label of the remove button, e.g. "Remove the Brooklyn filter". Only used with onRemove.
disableddefaultfalsebooleanDims the badge (a count too) and hides the remove button. With asChild the link gets aria-disabled, leaves the tab order and ignores clicks.
asChilddefaultfalsebooleanRenders your single child (an <a>, a router Link, a button) as the badge, with a hover tint, a press dip and a focus ring. asChild badges have no remove button, since a button inside a link is invalid. The demo swaps in a link.
countnumberShows this number instead of children and animates every change: the digits slide, the width follows on a spring and the badge bumps when it grows. Negative values clamp to 0, fractions round. The demo's bell drives it with the + and - buttons.
leftIconReactNodeAn icon before the label. Unsized svgs follow the size (12 / 12 / 14px).
rightIconReactNodeAn icon after the label, like an arrow on a link badge.
onRemove() => voidShows a round x button that calls it on click, tap, Enter, Backspace or Delete. The click does not bubble, so a chip inside a clickable row removes itself without opening the row.
classNamesPartial<Record<"dot" | "icon" | "label" | "count" | "remove", string>>Extra classes for the inner parts: the status dot, each icon wrapper, the label, the count and the remove button.
classNamestringExtra classes for the badge itself, merged last. Override the tone vars here, e.g. [--tone-foreground:white].
Parts
BadgeAnchor
componentPins a badge to a corner of its child (an icon button, an avatar). badge: the Badge to place (a count, or an empty Badge for a dot). placement: "top-end" (default) | "top-start" | "bottom-end" | "bottom-start", logical so it flips in RTL. offset: px, default 0; positive moves the badge inward, about 5 to 6px puts it on the edge of a round 40px host. invisible: scales it out and back in. ringColor: default var(--background), the 2px ring that cuts the badge out of its host (pass var(--surface) on panels). classNames.badge: the placed wrapper, which lets clicks through to the host. Props: BadgeAnchorProps.
Examples
Variants and tones
Three variants across the six tones, each with a status it would really carry.
Sizes and radii
20, 24 and 28px. Pills by default; the square steps suit tag-like labels.
Dot and pulse
A status dot, and a ping for anything live. An empty badge is a dot on its own.
Counts
Digits roll with the change and 128 reads 99+. The last row is de-DE with max 9999. Click a row to clear it.
Removable filter chips
Click the x or focus it and press Delete; the rest slide into place and focus moves to the next chip.
Anchored to an icon or avatar
A count on a bell, dots for unread and online, cut out by a ring in the panel's color.
Custom color and links
Any CSS color, and asChild for a badge that is a real link with hover, press and focus.
Long labels
A badge never grows past its container.
Files
- components/ui/badge.tsx
FAQ
Is the React Badge component free?
Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.
How do I install Badge?
Run npx wingo-ui@latest add badge. It also adds the Wingo UI items it builds on (Motion presets, Tones and UIProvider) and the npm packages @radix-ui/react-slot, lucide-react and motion. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/badge.json.
See also
Used in
More React data display components
Articles using this component
- Tailwind v4 Design Tokens: Colors, Dark Mode and Contrast
Next.js and Tailwind CSS20 min read