Badge
A short, non-interactive label that tags an object with a status, category or count.
import { Badge, BadgeDot } from "@/components/ui/badge"
export default function BadgeDemo() {
return (
<div className="flex flex-wrap items-center justify-center gap-2">
<Badge variant="success">
<BadgeDot />
Operational
</Badge>
<Badge variant="brand">New</Badge>
<Badge variant="secondary">v1.0.0</Badge>
<Badge variant="outline">Draft</Badge>
</div>
)
}Anatomy
- 1DotOptional. A status dot in the badge's color that can pulse for ongoing states.
- 2ContainerA small label with rounded corners. Semantic variants tint it with the status colors; outline adds a border instead of a fill.
- 3LabelA word or two in sentence case: a status, a count or a category.
- 4IconOptional. Sized by the badge, which tightens its padding on that side.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/badge.jsonUsage
import { Badge, BadgeDot } from "@/components/ui/badge"<Badge variant="success">
<BadgeDot />
Paid
</Badge>Examples
Variants
Variants map to meaning, not to taste. The four status variants — success, warning, destructive, info — are reserved for state, so a green badge always means the same thing everywhere in a product.
import { Badge } from "@/components/ui/badge"
export default function BadgeVariants() {
return (
<div className="flex max-w-lg flex-wrap items-center justify-center gap-2">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="brand">Brand</Badge>
<Badge variant="brand-secondary">Brand secondary</Badge>
<Badge variant="success">Success</Badge>
<Badge variant="warning">Warning</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="info">Info</Badge>
<Badge variant="ghost">Ghost</Badge>
<Badge variant="link">Link</Badge>
</div>
)
}Sizes
default (20px) fits alongside body text and in table cells. Use sm for counters inside navigation and lg for badges that sit next to headings.
import { Badge } from "@/components/ui/badge"
export default function BadgeSizes() {
return (
<div className="flex items-center gap-3">
<Badge variant="brand" size="sm">
Small
</Badge>
<Badge variant="brand">Default</Badge>
<Badge variant="brand" size="lg">
Large
</Badge>
</div>
)
}Status
Pair status badges with BadgeDot. The dot makes a column of states scannable at a glance; the word makes the state unambiguous. Set live to pulse the dot for activity in progress, such as a build or a sync. A steady state keeps a still dot, however important it is. The pulse stops when reduced motion is on.
- API
- Dashboard
- Webhooks
- Search index
import { Badge, BadgeDot } from "@/components/ui/badge"
const services = [
{ name: "API", status: "Operational", variant: "success", live: false },
{ name: "Dashboard", status: "Degraded", variant: "warning", live: false },
{ name: "Webhooks", status: "Outage", variant: "destructive", live: false },
{ name: "Search index", status: "Reindexing", variant: "info", live: true },
] as const
export default function BadgeStatus() {
return (
<ul className="flex w-full max-w-sm flex-col divide-y rounded-xl border bg-card">
{services.map((service) => (
<li
key={service.name}
className="flex items-center justify-between px-4 py-3 text-sm"
>
<span className="font-medium">{service.name}</span>
<Badge variant={service.variant}>
<BadgeDot live={service.live} />
{service.status}
</Badge>
</li>
))}
</ul>
)
}With icons
Mark icons with data-icon="inline-start" or data-icon="inline-end"; the badge tightens its padding on that side and sizes the icon to 12px, or 14px at lg.
import {
ArrowUpRightIcon,
CircleCheckIcon,
LockIcon,
SparklesIcon,
TrendingUpIcon,
} from "lucide-react"
import { Badge } from "@/components/ui/badge"
export default function BadgeWithIcon() {
return (
<div className="flex max-w-md flex-wrap items-center justify-center gap-2">
<Badge variant="success">
<CircleCheckIcon data-icon="inline-start" />
Verified
</Badge>
<Badge variant="brand">
<SparklesIcon data-icon="inline-start" />
AI generated
</Badge>
<Badge variant="secondary">
<LockIcon data-icon="inline-start" />
Private
</Badge>
<Badge variant="outline">
<TrendingUpIcon data-icon="inline-start" />
+12.4%
</Badge>
<Badge variant="info">
Changelog
<ArrowUpRightIcon data-icon="inline-end" />
</Badge>
</div>
)
}As a link
A badge can navigate — to a changelog entry, a filtered list, a tag. Use the render prop so it becomes a real link; link badges pick up a hover state automatically.
import { ArrowRightIcon } from "lucide-react"
import Link from "next/link"
import { Badge } from "@/components/ui/badge"
const tags = [
{ label: "color", href: "/docs/foundations/color" },
{ label: "typography", href: "/docs/foundations/typography" },
{ label: "accessibility", href: "/docs/foundations/accessibility" },
{ label: "design-tokens", href: "/docs/foundations/tokens" },
]
export default function BadgeAsLink() {
return (
<div className="flex flex-col items-center gap-4">
<Badge
variant="brand"
size="lg"
render={<Link href="/docs/resources/changelog" />}
>
See what's new
<ArrowRightIcon data-icon="inline-end" />
</Badge>
<div className="flex flex-wrap justify-center gap-1.5">
{tags.map((tag) => (
<Badge
key={tag.label}
variant="secondary"
render={<Link href={tag.href} />}
>
#{tag.label}
</Badge>
))}
</div>
</div>
)
}<Badge variant="brand" render={<Link href="/docs/resources/changelog" />}>
See what's new
</Badge>Counts
Counts label a destination with how much is waiting there. Cap them at 99+, use the solid default variant only for unread items that need action, and give the number context for screen readers.
import { BellIcon, InboxIcon, MessageSquareIcon } from "lucide-react"
import { Badge } from "@/components/ui/badge"
const items = [
{ label: "Inbox", icon: InboxIcon, count: 12, unread: true },
{ label: "Mentions", icon: MessageSquareIcon, count: 3, unread: true },
{ label: "Notifications", icon: BellIcon, count: 128, unread: false },
]
export default function BadgeCount() {
return (
<nav
aria-label="Mailboxes"
className="flex w-56 flex-col gap-px rounded-xl border bg-card p-1 shadow-raised"
>
{items.map(({ label, icon: Icon, count, unread }) => (
<a
key={label}
href="#"
className="flex h-9 items-center gap-2.5 rounded-lg px-2.5 text-sm hover:bg-accent"
>
<Icon className="size-4 text-muted-foreground" aria-hidden="true" />
<span className="flex-1">{label}</span>
<Badge
variant={unread ? "default" : "secondary"}
size="sm"
className="min-w-5 tabular-nums"
>
{count > 99 ? "99+" : count}
<span className="sr-only">
{unread ? " unread messages" : " messages in total"}
</span>
</Badge>
</a>
))}
</nav>
)
}Guidelines
When to use
- To show the state of an object: a payment, a deployment, a service.
- To add short metadata: a role, a version, a plan, a tag.
- To show a count of items waiting behind a navigation item.
When not to use
- To trigger an action — use a Button.
- To filter a view — use a Toggle Group or checkboxes, so the selected state is announced.
- To explain something — if it needs more than two words, it needs a sentence, an Alert or a Tooltip.
Never color alone
Keep it short
Restraint
Badges work because they are rare. If every row in a table carries three badges, none of them is noticed — keep one status badge per object and move the rest of the metadata into plain text.
Accessibility
- Static by default. A badge renders a
<span>and is not focusable. When it navigates, render it as a link so it gets a role, focus and a visible focus ring. - Status is text. Every status variant pairs a color with a word.
BadgeDotisaria-hidden; the label carries the meaning. - Counts need context. "12" alone is ambiguous when read aloud. Add a visually hidden suffix —
<span className="sr-only"> unread</span>— or put the count in the link's accessible name. - Contrast. Soft variants use step 11 text on step 3 fills, which the palette guarantees at 4.5:1 or more in both modes. Solid variants meet 4.5:1 as well.
- Motion. The
livepulse is decorative and is disabled underprefers-reduced-motion.
API reference
Badge
Renders a <span>. Built on Base UI's useRender, so it accepts a render prop.
BadgeDot
A 6px status dot that takes the badge's text color.
badgeVariants
The class generator is exported so other elements — a table cell, a custom link — can share badge styles.
import { badgeVariants } from "@/components/ui/badge"
<span className={badgeVariants({ variant: "info", size: "sm" })}>Beta</span>