Badge

( Label / Status / Indicator )

A small label for statuses, categories, or counts, in soft tinted or solid fills.

Live preview and source code for Badge

PendingIn progress
SubmittedIn reviewSuccess
FailedExpired
LiveNew99+v2.4.1

A small label for statuses, categories and counts: the status pill you reach for in a table row, a card header or next to a title. Soft tinted backgrounds by default, a solid fill when it needs to carry across a busy interface, and five colour intents that cover most of what a status ever needs to say. It renders a single <span> with inline styles, so it drops into any React project without a config file.

Installation

Copy the source from the Code tab above into your project as Badge.tsx. It is one file of TypeScript and React with inline styles, so there is no Tailwind config, no CSS import and no theme provider to set up first.

The only import beyond React is lucide-react, and it is a type-only import for the icon prop:

Terminal
npm install lucide-react

You need the package installed at runtime only if you actually pass an icon. If you never do, delete the import and the icon prop and the file stands on React alone.

Usage

Every prop is optional. The label is a prop rather than a child, so there is nothing to nest:

Example.tsx
import Badge from "./components/Badge";

export default function Example() {
    return <Badge label="Shipped" variant="success" dot />;
}

When to reach for it

A row of statuses in a table, where colour has to make the state readable at a glance while the word still does the actual work. success, warning, destructive and info are shaped for exactly that.

Metadata beside a heading: a version number, a plan tier, a content category. The sm size and pill shape are meant for sitting next to text without crowding it.

A count or a short tag inside a card, where a full chip component would be more structure than the thing deserves.

When not to

If it is clickable, it is not a badge. This renders a <span>, so it has no focus ring, no keyboard handling and no pressed state. A removable filter chip or a tag that navigates somewhere needs a <button> or an <a>, and reaching for Button is the better call.

If the state changes while the user is looking elsewhere, a badge alone will not tell them. Announce the change from a live region and let the badge show the result.

If you need more than a word or two, the fixed padding and nowrap will work against you. That is a callout or an alert, not a badge.

Accessibility

The leading dot is aria-hidden, deliberately: it is decoration, and the label is what carries the meaning. That is the important property here: colour and the dot are both redundant signals, so a screen reader and a person who cannot distinguish the tints both get the same information from the text.

Which means the one thing you have to get right is the label itself. Failed works; a bare ! or a colour with no word does not.

The badge renders as ordinary inline text with no ARIA role, which is correct for a static label. If it is reporting something that updates, put the live region on the container around it rather than on the badge.

Props

Every prop the Badge component accepts, with its type and default value.

Props accepted by the Badge component
PropTypeDefaultDescription
labelstringBadgeText rendered inside the badge.
variant"default" | "success" | "warning" | "destructive" | "info"defaultVisual style of the badge.
size"sm" | "md" | "lg"mdSize of the badge.
dotbooleanfalseShows a small leading status dot.
iconLucideIcon | nullnulllucide-react icon component rendered before the label.
solidbooleanfalseSolid fill instead of the default soft tinted background.
pillbooleantrueFully rounded pill instead of a soft rectangle.

Notes

dot and icon are not mutually exclusive. Set both and you get the dot first, then the icon, then the label, in that order.

style is spread after the component's own inline styles, so anything you pass wins. That is the escape hatch for a colour the five variants do not cover, and it is why the variant palette is deliberately small rather than configurable.

Every other prop lands on the underlying <span>, so title, data-* and event handlers all pass straight through.