Badge
A small status label with a variant × tone × size matrix: 3 variants, 6 tones, 3 sizes, plus asChild composition.
import { CircleCheck, TriangleAlert } from "lucide-react"
import { Badge } from "@/components/ui/badge"
export default function BadgeDemo() {
return (
<div className="flex items-center gap-2">
<Badge tone="success">
<CircleCheck aria-hidden="true" />
Live
</Badge>
<Badge tone="warning">
<TriangleAlert aria-hidden="true" />
Degraded
</Badge>
<Badge variant="outline" tone="brand" size="lg">
v2.4.0
</Badge>
<Badge variant="solid" tone="danger" size="sm">
3
</Badge>
</div>
)
}Installation
Run the following command
npx shadcn@latest add @ai2/badgeDependencies, the @ai2/tokens theme and the component file are installed together.
Install dependencies
npm install class-variance-authority@^0.7.1 radix-ui@^1.6.1Add the cn util
lib/utils.tsimport { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
/* Adds a source-attribution ref param to a URL (the inspiration exports mark their
outbound links with an ai2.design attribution). An invalid URL is returned as is.
This file is SHOWN TO THE CONSUMER: the docs component pages render the source of
`cn` in a code block, so a Turkish comment here would reach every one of those
pages. Keep it English. */
export function withRef(url: string, ref = "ai2.design"): string {
try {
const u = new URL(url)
u.searchParams.set("ref", ref)
return u.toString()
} catch {
return url
}
}Copy the source code
components/ui/badge.tsximport type * as React from "react"
import { Slot } from "@/components/ui/primitives"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex w-fit shrink-0 items-center justify-center gap-1 whitespace-nowrap border font-medium [&>svg]:pointer-events-none [&>svg]:size-3 [&>svg]:shrink-0 [&>i]:pointer-events-none [&>i]:text-xs [&>i]:leading-none",
{
variants: {
variant: {
solid: "border-transparent",
soft: "border-transparent",
outline: "bg-transparent",
},
tone: {
neutral: "",
brand: "",
success: "",
warning: "",
danger: "",
info: "",
},
size: {
sm: "h-5 rounded-md px-1.5 text-xs",
md: "h-6 rounded-md px-2 text-xs",
lg: "h-7 rounded-lg px-2.5 text-sm",
},
},
compoundVariants: [
{ variant: "solid", tone: "neutral", class: "bg-primary text-primary-foreground" },
{ variant: "solid", tone: "brand", class: "bg-brand text-brand-foreground" },
{ variant: "solid", tone: "success", class: "bg-success text-success-foreground" },
{ variant: "solid", tone: "warning", class: "bg-warning text-warning-foreground" },
{ variant: "solid", tone: "danger", class: "bg-danger text-danger-foreground" },
{ variant: "solid", tone: "info", class: "bg-info text-info-foreground" },
{ variant: "soft", tone: "neutral", class: "bg-secondary text-secondary-foreground" },
{ variant: "soft", tone: "brand", class: "bg-brand-soft text-brand-soft-foreground" },
{ variant: "soft", tone: "success", class: "bg-success-soft text-success-soft-foreground" },
{ variant: "soft", tone: "warning", class: "bg-warning-soft text-warning-soft-foreground" },
{ variant: "soft", tone: "danger", class: "bg-danger-soft text-danger-soft-foreground" },
{ variant: "soft", tone: "info", class: "bg-info-soft text-info-soft-foreground" },
{ variant: "outline", tone: "neutral", class: "border-border text-foreground" },
{ variant: "outline", tone: "brand", class: "border-brand/40 text-brand" },
{ variant: "outline", tone: "success", class: "border-success/40 text-success" },
{ variant: "outline", tone: "warning", class: "border-warning/50 text-warning-soft-foreground" },
{ variant: "outline", tone: "danger", class: "border-danger/40 text-danger" },
{ variant: "outline", tone: "info", class: "border-info/40 text-info" },
],
defaultVariants: {
variant: "soft",
tone: "neutral",
size: "md",
},
}
)
interface BadgeProps
extends React.ComponentProps<"span">,
VariantProps<typeof badgeVariants> {
asChild?: boolean
}
function Badge({ className, variant, tone, size, asChild = false, ...props }: BadgeProps) {
const Comp = asChild ? Slot.Root : "span"
return (
<Comp
data-slot="badge"
data-tone={tone ?? "neutral"}
className={cn(badgeVariants({ variant, tone, size, className }))}
{...props}
/>
)
}
export { Badge, badgeVariants, type BadgeProps }Manual installs skip the @ai2/tokens theme, so add the token CSS from the theming guide or the tone colors will be missing.
Usage
import { Badge } from "@/components/ui/badge"
<Badge variant="outline" tone="success">
Live
</Badge>All three axes are optional. Omit them and you get the neutral soft medium badge. Any combination of the axes is valid.
Examples
Variants
Use soft for status pills, solid for counts and outline for subtle tags.
Tones
Tones combine with every variant: variant="outline" tone="danger" is a prop pair, not custom CSS.
Sizes
sm (h-5) fits dense tables, md (h-6) is the default, and lg (h-7) suits standalone tags.
As child
With asChild, the badge must have exactly one element child, useful for linkable version tags and filters.
Props
Badge also accepts every native <span> prop.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "solid" | "soft" | "outline" | "soft" | Visual style of the badge. |
tone | "neutral" | "brand" | "success" | "warning" | "danger" | "info" | "neutral" | Semantic color channel, mapped to theme tokens. |
size | "sm" | "md" | "lg" | "md" | Height, padding and radius scale. |
asChild | boolean | false | Merges props onto the child element (e.g. a Link) instead of rendering a native span. |
ai2 Badge: tone-driven status labels for React
The ai2 Badge is a shadcn-compatible react badge component styled with Tailwind CSS v4. It is the smallest tone-aware surface in the system: 3 variants, 6 tones and 3 sizes make 54 combinations for status pills, counts, version tags and category chips. A green "Live" indicator or a red error count is a prop pair, not custom CSS.
It ships through the shadcn registry format, so you install it with one CLI command, an MCP agent, or a copy-paste, and the TypeScript source lands in your own project. You own the file; there is no runtime dependency on ai2 itself. The live example above is the exact component you get.
What is the ai2 Badge?
A single Badge component (rendered as a span) plus an exported badgeVariants cva function. Three typed axes drive the look: variant (solid, soft, outline), tone (neutral, brand, success, warning, danger, info) and size (sm, md, lg). The default is a neutral soft medium badge.
The six tones are what make it an indicator, not just a label: success for healthy states, warning for degraded ones, danger for failures, info for notices, brand for product accents and neutral for everything else. Each tone resolves to the ai2 token file, so badges follow your theme in light and dark mode, and asChild lets a badge render as a link or any other element.
Why use it
- Semantic tones for status: Status UIs live and die by consistent color coding. The six tones map to the same semantic tokens as Button and Alert, so "success" is the same green everywhere in your product.
- Three intensities per tone: solid for high-emphasis counts, soft for the classic status pill, outline for subtle tags. Same tone, three volumes.
- Linkable via asChild: asChild merges the badge styling onto a child element, so version tags and filter chips can be real anchors without wrapper hacks.
- Icon-ready sizing: svg children are sized to size-3 with a built-in gap, so a dot or check icon sits correctly next to the label with no extra classes.
- Agent-readable metadata: The registry item describes the variant matrix and intended use in plain words, so an MCP agent can pick the right tone without guessing.
Features
- 3 variants x 6 tones x 3 sizes: 54 combinations generated by cva compound variants, all resolved from theme tokens rather than hardcoded colors.
- Soft default: The default variant is soft with the neutral tone, the most common status-pill look, so the zero-prop badge is already useful.
- asChild composition: Built on the radix Slot primitive, the badge can render as a Next.js Link, an anchor or any single element child.
- Data attributes for styling: The root exposes data-slot="badge" and data-tone, so you can restyle specific tones from CSS without forking the component.
- Whitespace-safe layout: w-fit, shrink-0 and whitespace-nowrap keep badges from collapsing or wrapping inside flex rows and table cells.
- TypeScript source: The file you install is typed end to end, including the exported badgeVariants function for use on other elements.
Production tips
- Keep one tone per meaning: Decide once what success, warning and danger mean in your product (deploy states, payment states) and reuse the mapping everywhere. Mixed meanings destroy scannability.
- Do not rely on color alone: Pair the tone with a label ("Live", "Failed") or an icon. Color-blind users and monochrome contexts need the text to carry the meaning.
- Use sm in dense tables: The sm size (h-5) keeps rows compact; md (h-6) reads better in cards and headers, and lg (h-7) suits standalone tags. Keep one size per column.
- Prefer outline for passive tags: Categories and metadata read best as outline badges; reserve solid and soft tones for states that can change, so movement catches the eye.
- Counts belong in solid: Notification counts and totals get the strongest contrast from variant="solid", especially solid danger for error counts.
Works with the rest of ai2
Badges annotate almost everything in the registry. Put one next to a trigger in an ai2 Accordion to show counts, in an ai2 Avatar stack for presence, or in ai2 Table cells for row status.
For richer surfaces, drop badges into ai2 Card headers for plan tiers, or next to ai2 Tabs labels for unread counts. Everything shares one token source, so a danger badge inside a success alert still resolves to the same palette in both modes.