Draft Active Pending review Failed New
import { Badge } from "@/components/ui/badge" ;
export default function BadgeDemo () {
return (
< div className = "flex flex-wrap items-center gap-2" >
< Badge >Draft</ Badge >
< Badge color = "success" >Active</ Badge >
< Badge color = "warning" >Pending review</ Badge >
< Badge color = "danger" >Failed</ Badge >
< Badge variant = "solid" color = "primary" >
New
</ Badge >
</ div >
);
}
CLI Manual
$ pnpm dlx shadcn@latest add @desyne/badge
The CLI installs dependencies and any other components this one uses.
import { Badge } from "@/components/ui/badge" ;
< Badge color = "success" >Active</ Badge >
A badge with no props is a neutral soft label. It renders a plain <span> with no client JavaScript, so it works in server components.
Badge: a short, read-only label that describes something next to it, such as a status, a count, a version or a category.
Tag Group : labels the user can select, remove or navigate with the keyboard.
Button : anything that performs an action. If a badge needs to be clickable, render a link or button with badgeVariants instead.
Alert : a message that needs a sentence of explanation.
Keep badge text to one or two words. Rely on the text, not the color alone, to carry the meaning.
< Badge >
< Icon /> { /* optional, auto-sized to 12px */ }
Label
</ Badge >
Part Renders Notes Badge<span>Root. Inline flex, no wrapping, tabular-nums for counts. Carries data-slot="badge". Icon <svg>Any direct svg child is sized to 12px and ignores pointer events. Dot ::beforeDrawn by the dot variant in the tone color. Not a separate element.
soft (the default) suits most labels, solid draws the eye for "New" or unread counts, outline is the quietest, and dot pairs a colored dot with neutral text. Every variant works with every color; neutral is the default color.
solid soft outline dot primary primary primary primary primary
brand brand brand brand brand
neutral neutral neutral neutral neutral
success success success success success
warning warning warning warning warning
danger danger danger danger danger
info info info info info
import { Badge } from "@/components/ui/badge" ;
const colors = [
"primary" ,
"brand" ,
"neutral" ,
"success" ,
"warning" ,
"danger" ,
"info" ,
] as const ;
const variants = [ "solid" , "soft" , "outline" , "dot" ] as const ;
export default function BadgeVariants () {
return (
< div className = "w-full max-w-2xl overflow-x-auto" >
< div className = "grid w-max grid-cols-[auto_repeat(4,auto)] items-center gap-2 text-xs" >
< span />
{variants. map (( v ) => (
< span
key = {v}
className = "text-center text-muted-foreground capitalize"
>
{v}
</ span >
))}
{colors. map (( color ) => (
< div key = {color} className = "contents" >
< span className = "pr-2 text-muted-foreground capitalize" >
{color}
</ span >
{variants. map (( variant ) => (
< Badge
key = {variant}
variant = {variant}
color = {color}
className = "justify-self-center capitalize"
>
{color}
</ Badge >
))}
</ div >
))}
</ div >
</ div >
);
}
md (24px) is the default. Use sm (20px) inside table cells, list rows, buttons and next to small text.
import { Badge } from "@/components/ui/badge" ;
export default function BadgeSizes () {
return (
< div className = "flex flex-wrap items-center gap-2" >
< Badge size = "sm" color = "info" >
Small
</ Badge >
< Badge size = "md" color = "info" >
Medium
</ Badge >
< Badge size = "sm" variant = "dot" color = "success" >
Online
</ Badge >
< Badge size = "md" variant = "dot" color = "success" >
Online
</ Badge >
</ div >
);
}
shape="pill" fully rounds the badge. For a round count, combine pill with min-w-5 px-1 so single digits stay circular.
import { Badge } from "@/components/ui/badge" ;
export default function BadgeShapes () {
return (
< div className = "flex flex-wrap items-center gap-2" >
< Badge color = "primary" >Default</ Badge >
< Badge color = "primary" shape = "pill" >
Pill
</ Badge >
< Badge variant = "outline" shape = "pill" >
v2.4.0
</ Badge >
< Badge
variant = "solid"
color = "danger"
shape = "pill"
size = "sm"
className = "min-w-5 px-1"
>
9
</ Badge >
</ div >
);
}
Put a lucide icon before the label. It's sized and spaced automatically, and adds recognition without relying on color.
Verified Degraded 2h ago Private AI
import {
AlertTriangleIcon,
BadgeCheckIcon,
ClockIcon,
LockIcon,
SparklesIcon,
} from "lucide-react" ;
import { Badge } from "@/components/ui/badge" ;
export default function BadgeWithIcon () {
return (
< div className = "flex flex-wrap items-center gap-2" >
< Badge color = "success" shape = "pill" >
< BadgeCheckIcon /> Verified
</ Badge >
< Badge color = "warning" >
< AlertTriangleIcon /> Degraded
</ Badge >
< Badge variant = "outline" >
< ClockIcon /> 2h ago
</ Badge >
< Badge variant = "outline" >
< LockIcon /> Private
</ Badge >
< Badge variant = "solid" color = "brand" >
< SparklesIcon /> AI
</ Badge >
</ div >
);
}
variant="dot" keeps the text neutral and puts the meaning in a small dot. It reads calmly in dense status columns where a column of colored backgrounds would be noisy.
Operational Degraded Outage Maintenance Paused
import { Badge } from "@/components/ui/badge" ;
const statuses = [
{ label: "Operational" , color: "success" },
{ label: "Degraded" , color: "warning" },
{ label: "Outage" , color: "danger" },
{ label: "Maintenance" , color: "info" },
{ label: "Paused" , color: "neutral" },
] as const ;
export default function BadgeDot () {
return (
< div className = "flex flex-wrap items-center gap-2" >
{statuses. map (( s ) => (
< Badge key = {s.label} variant = "dot" color = {s.color}>
{s.label}
</ Badge >
))}
</ div >
);
}
Position a badge over an icon button for unread counts, or place it inline inside a button. When the badge overlays an icon button, include the count in the button's aria-label and hide the badge with aria-hidden so it isn't announced twice.
import { BellIcon, InboxIcon } from "lucide-react" ;
import { Badge } from "@/components/ui/badge" ;
import { Button } from "@/components/ui/button" ;
export default function BadgeCount () {
return (
< div className = "flex items-center gap-4" >
< div className = "relative" >
< Button
variant = "outline"
size = "icon"
aria-label = "Notifications, 3 unread"
>
< BellIcon />
</ Button >
< Badge
aria-hidden
variant = "solid"
color = "danger"
shape = "pill"
size = "sm"
className = "absolute -top-1.5 -right-1.5 min-w-5 px-1 ring-2 ring-background"
>
3
</ Badge >
</ div >
< Button variant = "ghost" >
< InboxIcon /> Inbox
< Badge size = "sm" shape = "pill" className = "ml-1" >
128
</ Badge >
</ Button >
</ div >
);
}
Badge renders a <span> and isn't interactive. For clickable labels, apply badgeVariants to a React Aria Link (or a Next.js Link) and add hover and focus styles.
import { Link } from "react-aria-components" ;
import { badgeVariants } from "@/components/ui/badge" ;
const topics = [ "accessibility" , "react" , "design-tokens" , "tailwind" ];
export default function BadgeAsLink () {
return (
< nav aria-label = "Topics" className = "flex flex-wrap items-center gap-2" >
{topics. map (( topic ) => (
< Link
key = {topic}
href = { `#${ topic }` }
className = { badgeVariants ({
variant: "outline" ,
shape: "pill" ,
className:
"cursor-pointer outline-none data-focus-visible:ring-[3px] data-focus-visible:ring-ring/25 data-hovered:bg-muted" ,
})}
>
#{topic}
</ Link >
))}
</ nav >
);
}
Colors are applied through the --tone and --tone-fg variables, so any color works as a one-off. Pass a non-neutral color (neutral soft and outline use fixed surface colors) and override --tone; for solid, also set --tone-fg for the text.
Design Engineering Marketing Sales
import { Badge } from "@/components/ui/badge" ;
export default function BadgeCustomColor () {
return (
< div className = "flex flex-wrap items-center gap-2" >
< Badge color = "primary" className = "[--tone:var(--color-violet-600)]" >
Design
</ Badge >
< Badge color = "primary" className = "[--tone:var(--color-teal-600)]" >
Engineering
</ Badge >
< Badge
variant = "solid"
color = "primary"
className = "[--tone-fg:white] [--tone:var(--color-pink-600)]"
>
Marketing
</ Badge >
< Badge
variant = "dot"
color = "primary"
className = "[--tone:var(--color-orange-500)]"
>
Sales
</ Badge >
</ div >
);
}
An environment badge and a fixed-width status dot badge per row, so the status column lines up.
Add usage-based billing
main · 4m ago
Production Ready Export audit log as CSV
feat/audit-log · 12m ago
Preview Building Retry failed webhooks
fix/webhook-retry · 1h ago
Preview Error Upgrade to React 19.2
main · 3h ago
Production Canceled
import { GitBranchIcon } from "lucide-react" ;
import { Badge } from "@/components/ui/badge" ;
const deployments = [
{
id: "dpl_8f2k" ,
branch: "main" ,
message: "Add usage-based billing" ,
env: "Production" ,
status: "Ready" ,
time: "4m ago" ,
},
{
id: "dpl_7c1a" ,
branch: "feat/audit-log" ,
message: "Export audit log as CSV" ,
env: "Preview" ,
status: "Building" ,
time: "12m ago" ,
},
{
id: "dpl_6b9e" ,
branch: "fix/webhook-retry" ,
message: "Retry failed webhooks" ,
env: "Preview" ,
status: "Error" ,
time: "1h ago" ,
},
{
id: "dpl_5a3d" ,
branch: "main" ,
message: "Upgrade to React 19.2" ,
env: "Production" ,
status: "Canceled" ,
time: "3h ago" ,
},
] as const ;
const statusColor = {
Ready: "success" ,
Building: "warning" ,
Error: "danger" ,
Canceled: "neutral" ,
} as const ;
export default function BadgeRecipeDeployments () {
return (
< ul className = "w-full max-w-lg divide-y rounded-lg border bg-card" >
{deployments. map (( d ) => (
< li key = {d.id} className = "flex items-center gap-3 px-4 py-3" >
< div className = "flex min-w-0 flex-1 flex-col gap-1" >
< p className = "truncate font-medium text-sm" >{d.message}</ p >
< p className = "flex items-center gap-1.5 text-muted-foreground text-xs" >
< GitBranchIcon className = "size-3" aria-hidden />
< span className = "truncate font-mono" >{d.branch}</ span >
< span aria-hidden >·</ span >
{d.time}
</ p >
</ div >
< Badge
size = "sm"
variant = "outline"
color = {d.env === "Production" ? "brand" : "neutral" }
>
{d.env}
</ Badge >
< Badge
variant = "dot"
color = {statusColor[d.status]}
className = "w-24 justify-start"
>
{d.status}
</ Badge >
</ li >
))}
</ ul >
);
}
Pill counts at the end of navigation links. Unread counts use solid to stand out; totals stay soft.
import {
ArchiveIcon,
FileTextIcon,
InboxIcon,
SendIcon,
StarIcon,
} from "lucide-react" ;
import { Link } from "react-aria-components" ;
import { Badge } from "@/components/ui/badge" ;
const items = [
{ label: "Inbox" , icon: InboxIcon, count: 24 , unread: true , current: true },
{ label: "Starred" , icon: StarIcon, count: 3 , unread: false },
{ label: "Drafts" , icon: FileTextIcon, count: 2 , unread: false },
{ label: "Sent" , icon: SendIcon },
{ label: "Archive" , icon: ArchiveIcon },
];
export default function BadgeRecipeNavCounts () {
return (
< nav aria-label = "Mailboxes" className = "w-full max-w-56" >
< ul className = "flex flex-col gap-0.5" >
{items. map (( item ) => (
< li key = {item.label}>
< Link
href = { `#${ item . label . toLowerCase () }` }
aria-current = {item.current ? "page" : undefined }
className = "flex h-8 items-center gap-2 rounded-md px-2 text-muted-foreground text-sm outline-none data-focus-visible:ring-[3px] data-focus-visible:ring-ring/25 data-hovered:bg-muted data-hovered:text-foreground aria-[current=page]:bg-muted aria-[current=page]:font-medium aria-[current=page]:text-foreground"
>
< item.icon className = "size-4" aria-hidden />
< span className = "flex-1" >{item.label}</ span >
{item.count !== undefined && (
< Badge
size = "sm"
shape = "pill"
variant = {item.unread ? "solid" : "soft" }
color = {item.unread ? "primary" : "neutral" }
>
{item.count}
</ Badge >
)}
</ Link >
</ li >
))}
</ ul >
</ nav >
);
}
A verified badge next to a name, and small outline badges for skills with one solid role badge.
MP Staff engineer, Platform · Joined March 2023
TypeScript Kubernetes Postgres Admin
import { BadgeCheckIcon } from "lucide-react" ;
import { Avatar } from "@/components/ui/avatar" ;
import { Badge } from "@/components/ui/badge" ;
export default function BadgeRecipeProfile () {
return (
< div className = "flex w-full max-w-md items-start gap-4 rounded-xl border bg-card p-4" >
< Avatar size = "xl" colorful alt = "Maya Patel" fallback = "MP" />
< div className = "flex min-w-0 flex-col gap-2" >
< div className = "flex flex-wrap items-center gap-2" >
< h3 className = "font-semibold text-base" >Maya Patel</ h3 >
< Badge size = "sm" shape = "pill" color = "info" >
< BadgeCheckIcon /> Verified
</ Badge >
</ div >
< p className = "text-muted-foreground text-sm" >
Staff engineer, Platform · Joined March 2023
</ p >
< div className = "flex flex-wrap gap-1.5" >
< Badge size = "sm" variant = "outline" >
TypeScript
</ Badge >
< Badge size = "sm" variant = "outline" >
Kubernetes
</ Badge >
< Badge size = "sm" variant = "outline" >
Postgres
</ Badge >
< Badge size = "sm" variant = "solid" color = "brand" >
Admin
</ Badge >
</ div >
</ div >
</ div >
);
}
A badge is a plain <span>; screen readers read its text inline with the surrounding content. It has no role and isn't focusable.
Don't rely on color alone. The text (or an icon plus text) must carry the meaning; the dot variant's dot is decorative.
For bare numbers, give context: "3" next to a bell icon means nothing when read out. Put the full phrase in the parent control's aria-label (for example "Notifications, 3 unread") and mark the badge aria-hidden, or add visually hidden text.
warning soft and outline badges darken the text in light mode so it keeps enough contrast on light backgrounds.
Clickable badges must be real links or buttons. See As a link .
Attribute Present when data-slot="badge"Always. Target badges from a parent with *:data-[slot=badge]:….
Variable Used for --toneBackground (solid), text, border and tint (soft, outline), dot (dot) --tone-fgText on solid badges
< Badge color = "primary" className = "[--tone:var(--color-violet-600)]" >
Design
</ Badge >
The shared tones map in @/lib/primitive holds the classes for each named color if you want to reuse them on your own elements.
The tailwind-variants function behind the component. Use it to style links, buttons or table cells as badges.
import { badgeVariants } from "@/components/ui/badge" ;
badgeVariants ({ variant: "outline" , color: "info" , size: "sm" , shape: "pill" });
Also accepts every prop of <span> except color.
Tag Group : interactive, removable labels.
Avatar : user images with a presence dot.
Table : status columns are a common home for dot badges.
Button : for anything clickable.