Shadcn Badge for React and Tailwind
A compact label for status, counts, and categories.
Installation
bunx --bun shadcn@latest add https://kit.dev/r/badge.jsonpnpm dlx shadcn@latest add https://kit.dev/r/badge.jsonnpx shadcn@latest add https://kit.dev/r/badge.jsonyarn shadcn@latest add https://kit.dev/r/badge.jsonInstall the following dependencies:
bun add @ark-ui/react tailwind-variantspnpm add @ark-ui/react tailwind-variantsnpm install @ark-ui/react tailwind-variantsyarn add @ark-ui/react tailwind-variantsImport the following variables into your CSS file
@theme inline {
--color-destructive-foreground: var(--destructive-foreground);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
--color-warning: var(--warning);
--color-warning-foreground: var(--warning-foreground);
}
:root {
--destructive-foreground: var(--color-red-700);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-700);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-700);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-700);
}
.dark {
--destructive-foreground: var(--color-red-400);
--info: var(--color-blue-500);
--info-foreground: var(--color-blue-400);
--success: var(--color-emerald-500);
--success-foreground: var(--color-emerald-400);
--warning: var(--color-amber-500);
--warning-foreground: var(--color-amber-400);
}Copy and paste the following code into your project.
"use client";
import { ark } from "@ark-ui/react/factory";
import { tv, type VariantProps } from "tailwind-variants";
import { cn } from "@/lib/utils";
export const badgeVariants = tv({
base: [
"relative",
"inline-flex items-center justify-center gap-1",
"select-none whitespace-nowrap font-medium text-xs",
"rounded-md border border-transparent",
"overflow-hidden",
"transition-colors",
"outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-ring/32",
"[&_svg]:pointer-events-none [&_svg]:size-3 [&_svg]:shrink-0",
"[button&,a&]:cursor-pointer [button&,a&]:pointer-coarse:after:absolute [button&,a&]:pointer-coarse:after:size-full [button&,a&]:pointer-coarse:after:min-h-11 [button&,a&]:pointer-coarse:after:min-w-11",
"motion-reduce:transition-none!",
],
variants: {
variant: {
default: [
"bg-foreground",
"text-background",
"focus-visible:border-foreground focus-visible:ring-foreground/20",
"dark:focus-visible:ring-foreground/40",
"[a&]:hover:bg-foreground/90",
],
secondary: [
"bg-secondary",
"text-secondary-foreground",
"border-secondary/20",
"focus-visible:border-foreground focus-visible:ring-foreground/50",
"[a&]:hover:bg-secondary/90",
],
outline: [
"text-foreground",
"border-border",
"[a&]:hover:bg-accent",
"[a&]:hover:text-accent-foreground",
],
success: [
"bg-success/10",
"text-success-foreground",
"border-success/20",
"focus-visible:border-success focus-visible:ring-success/20",
"[a&]:hover:bg-success/20",
],
info: [
"bg-info/10",
"text-info-foreground",
"border-info/20",
"focus-visible:border-info focus-visible:ring-info/50",
"[a&]:hover:bg-info/20",
],
warning: [
"bg-warning/10",
"text-warning-foreground",
"border-warning/20",
"focus-visible:border-warning focus-visible:ring-warning/20",
"dark:focus-visible:ring-warning/40",
"[a&]:hover:bg-warning/20",
],
destructive: [
"bg-destructive/10 dark:bg-destructive/5",
"text-destructive-foreground",
"border-destructive-foreground/20",
"focus-visible:border-destructive focus-visible:ring-destructive/24",
"dark:focus-visible:ring-destructive/40",
"[a&]:hover:bg-destructive/20",
],
},
size: {
sm: ["h-5 min-w-5", "px-1"],
md: ["h-5.5 min-w-5.5", "px-1.5"],
lg: ["h-6.5 min-w-6.5", "px-2", "text-sm"],
},
pill: {
true: [
"rounded-full",
"has-[>svg]:data-[size=sm]:pe-1.5",
"has-[>svg]:data-[size=md]:pe-2",
"has-[>svg]:data-[size=lg]:pe-2 sm:has-[>svg]:data-[size=lg]:pe-2.5",
],
},
},
defaultVariants: {
variant: "default",
size: "md",
pill: false,
},
});
export type BadgeVariant = VariantProps<typeof badgeVariants>["variant"];
export interface BadgeProps
extends React.ComponentProps<typeof ark.span>,
VariantProps<typeof badgeVariants> {}
export const Badge = (props: BadgeProps) => {
const {
variant = "default",
size = "md",
pill = false,
className,
...rest
} = props;
return (
<ark.span
className={cn(badgeVariants({ pill, size, variant }), className)}
data-pill={pill || undefined}
data-size={size}
data-slot="badge"
data-variant={variant}
{...rest}
/>
);
};Update the import paths to match your project setup.
Anatomy
A single element. Optional icons or a spinner are children, not separate parts.
Badge
└── content (text, icon, spinner, …)Usage
import { Badge } from "@/components/ui/badge";<Badge>Favorite</Badge>Keep the label short. Badge is for status, counts, and categories — not body copy.
Variants
variant sets color. Status colors (success, info, warning, destructive) use the CSS variables from installation.
Default
Secondary
Outline
Success
Info
Warning
Destructive
Sizes
size controls height and horizontal padding. Icons inside the badge are size-3 unless you set a size-* class on the SVG.
| Size | Height | Padding |
|---|---|---|
sm | h-5 (1.25rem) | px-1 |
md | h-5.5 (1.375rem) | px-1.5 |
lg | h-6.5 (1.625rem), text-sm | px-2 |
Small
Medium
Large
Pill
pill uses rounded-full. With an icon child, end padding increases so the glyph is not clipped.
Examples
With link
asChild merges badge styles onto a single child. Use it for Link or a.
With icon
Put the icon before the label. Decorative icons should be aria-hidden="true".
With spinner
Use Spinner for in-progress states. Hide it from assistive tech when the text already describes the status.
Count
Numeric counts work well with pill.
In a button
Place a badge after the button label. Negative end margin (-me-1) lines it up with the button padding.
Custom color
Override background, text, and border with className. Prefer semantic tokens when a variant already exists.
Guides
Badge vs Status vs Alert
| Badge | Status | Alert | |
|---|---|---|---|
| Role | Short label or count | Dot / indicator | Message the user should notice |
| Typical content | Text, optional icon | Empty or a tiny icon | Title, description, actions |
| Live region | No | No (aria-hidden) | role="alert" by default |
Use Announcement for an inline banner; nest a Badge there for the category chip.
asChild
asChild comes from Ark’s factory. The child must be a single element (a, Link, button). Badge classes, data-slot, data-variant, data-size, and data-pill merge onto that element.
<Badge asChild variant="info">
<Link href="/changelog">New</Link>
</Badge>When the child is a or button, hover styles apply and a 44×44 CSS hit area is added on coarse pointers.
Using badgeVariants
badgeVariants is the same tv() recipe the component uses. Apply it when you need badge chrome on an element you already own:
import { badgeVariants } from "@/components/ui/badge";
<span className={badgeVariants({ size: "sm", variant: "outline" })}>
Draft
</span>API Reference
shadcn.io Badge is a styled span (Ark factory), not a Zag machine. There is no context hook or root provider.
asChild merges props onto a single child element.
Badge
Root. Renders a span.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "outline" | "success" | "info" | "warning" | "destructive" | "default" | Color treatment. |
size | "sm" | "md" | "lg" | "md" | Height and padding. See Sizes. |
pill | boolean | false | Fully rounded ends. Extra end padding when an SVG child is present. |
asChild | boolean | false | Render the child element instead of a span. |
className | string | - | Class names on the root. |
Native span attributes (id, title, aria-*, …) pass through. There is no role by default.
| Attribute | Description |
|---|---|
data-slot | badge |
data-variant | "default", "secondary", "outline", "success", "info", "warning", or "destructive" |
data-size | "sm", "md", or "lg" |
data-pill | "true" when pill is set |
badgeVariants
Exported tv() recipe. Same variant, size, and pill keys as Badge.
badgeVariants({ variant: "secondary", size: "sm", pill: true });BadgeVariant
Type alias for the variant union: "default" | "secondary" | "outline" | "success" | "info" | "warning" | "destructive".
BadgeProps
Props of Badge: span attributes plus VariantProps<typeof badgeVariants>.
Accessibility
Badge is not a live region and is not a composite widget. The visible text is the accessible name.
- Decorative icons (the label already explains the meaning):
aria-hidden="true". - If the icon is the only cue (icon-only badge), set
aria-labelonBadge. - Do not use Badge as a button unless
asChildrenders a realbutton(or a link for navigation). Spinnerdefaults torole="status". Passaria-hidden="true"when adjacent text already describes the state.
Keyboard support
A default badge is not focusable.
| Key | Description |
|---|---|
Tab | Moves to a badge only when asChild renders a focusable element (a, button, …). |
Enter | Activates that focusable child. |
Space | Activates a button child. |