React Badge Examples — Variants, Icons, Counts, Status
Copy-paste React Badge examples — five variants, Lucide icon pairing, notification count overlays, table status cells, and data-driven plan tiers with Drivn.
A badge is the smallest piece of interface that still carries meaning — one or two words, a color, and a shape the eye resolves before it reads. Beta on a nav item, Failed in a deploy row, a red 3 on a bell icon: each one replaces a sentence of explanation with a glance.
Drivn's Badge is a single <span> in about thirty lines. No sub-components, no cva, no forwardRef, nothing at runtime past React and Tailwind. Its base class is inline-flex items-center gap-1.5 px-2.5 py-0.5 text-xs font-semibold rounded-full, and five variants — default, secondary, success, outline, destructive — each resolve to a token-backed class string in styles.variants. That gap-1.5 in the base is the reason an icon and a label line up without a wrapper div.
The five patterns below are the ones that keep reappearing in real applications: a variant reference, an icon-and-label status pill, a count overlay pinned to an icon button, a status cell inside a data table, and badges rendered from a server field through a mapping object. Each snippet assumes Drivn is installed via the CLI and Badge is imported from @/components/ui/badge. For the engineering trade-offs against shadcn/ui, read Drivn vs shadcn/ui Badge.
All five variants at a glance
Drivn's Badge ships five variants, each referencing a design token so colors adapt to your theme automatically. default uses the primary accent with a soft translucent background; secondary uses the secondary token for a lower-emphasis alternative; success uses the success token for green pills that signal healthy or active states; outline drops the fill entirely and renders only a border with muted text, appropriate for tags and categories; destructive uses the destructive token for error, failure, or danger states.
The variants are a single prop away — no cva, no forwardRef, no configuration. Drop the badge inline with any text, inside a card header, alongside a table cell, or as a decorator on a list item.
1 <Badge variant="default">Default</Badge> 2 <Badge variant="secondary">Secondary</Badge> 3 <Badge variant="success">Success</Badge> 4 <Badge variant="outline">Outline</Badge> 5 <Badge variant="destructive">Destructive</Badge>
Badge with a Lucide icon
Pairing an icon with a single word gives a badge roughly double the communicative density. A checkmark next to "Active", a warning triangle next to "Failed", a clock next to "Pending" — the icon registers preattentively and the word confirms the meaning.
Drivn's Badge has gap-1.5 built into its base class, so an icon and text render with consistent spacing without any flex wrapper or gap utility at the call site. Size the icon with w-3 h-3 (12 px) to match the text-xs label, and the pair sits balanced inside the px-2.5 py-0.5 padding of the badge container. Every variant inherits the same gap, so icon layout stays uniform regardless of color.
1 import { Check, CircleAlert, Clock } from 'lucide-react' 2 import { Badge } from '@/components/ui/badge' 3 4 <Badge variant="success"> 5 <Check className="w-3 h-3" /> 6 Active 7 </Badge> 8 <Badge variant="destructive"> 9 <CircleAlert className="w-3 h-3" /> 10 Failed 11 </Badge> 12 <Badge variant="secondary"> 13 <Clock className="w-3 h-3" /> 14 Pending 15 </Badge>
Status badge in a data table cell
Any table listing deployments, orders, invoices, or users ends up with a status column, and a color-coded pill is the fastest way to make that column scannable. Keep the status-to-variant mapping in one plain object beside the column definition, then index into it from the cell renderer — success for healthy or completed, destructive for failed, secondary for pending or in-flight, outline for archived or draft.
The data table hands the whole row to the cell renderer, so the mapping stays a pure lookup with no branching logic inside JSX. Typing the object as Record<Status, ...> makes the compiler flag a missing case the moment you widen the status union, which is what keeps a new status from silently rendering an undefined variant. Because the Badge is text-xs with py-0.5 padding, row heights stay flush with plain-text cells and no vertical-alignment class is needed. The same object works in a card list or a detail header, so one mapping serves every surface that shows the status.
1 type Status = 'active' | 'pending' | 'failed' | 'archived' 2 3 const variantFor: Record<Status, 'success' | 'secondary' | 'destructive' | 'outline'> = { 4 active: 'success', 5 pending: 'secondary', 6 failed: 'destructive', 7 archived: 'outline', 8 } 9 10 // In your column definition 11 { 12 accessorKey: 'status', 13 header: 'Status', 14 cell: ({ row }) => { 15 const status = row.original.status as Status 16 return <Badge variant={variantFor[status]}>{status}</Badge> 17 }, 18 }
Render badges from a server field
In real applications, badge content usually comes from a database row or API response — a user's plan tier, a feature's release state, the current deployment ring. Define a small mapping object so the label-and-variant transformation lives in one place, then pass the server value through it at render time.
For lists of many categorical tags — blog post categories, product filters, user skills — pair the outline variant with the same mapping pattern to render each tag consistently. If the list has an overflow cap, slice the array, render visible badges, and show a +N counter for the remainder using the same Badge component with a subdued variant. Because the mapping is a plain object, adding a new tier or tag is a one-line edit, and TypeScript's inference keeps the variant prop type aligned automatically.
1 const tiers = { 2 free: { label: 'Free', variant: 'outline' as const }, 3 pro: { label: 'Pro', variant: 'default' as const }, 4 enterprise: { label: 'Enterprise', variant: 'success' as const }, 5 } 6 7 export function UserPlanBadge({ 8 plan, 9 }: { plan: keyof typeof tiers }) { 10 const { label, variant } = tiers[plan] 11 return <Badge variant={variant}>{label}</Badge> 12 }
Install Drivn in one command
Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.
npx drivn@latest createRequires Node 18+. Works with npm, pnpm, and yarn.
Frequently asked questions
Five: default, secondary, success, outline, and destructive. Each variant maps to a Tailwind class string in the component's internal styles.variants object and references a CSS design token (primary, secondary, success, border, destructive). Because the colors come from tokens, switching your app's theme updates every badge instantly without touching individual call sites. Pass the variant name as a prop — <Badge variant="success"> — and TypeScript autocompletes the options.
Drop the icon component as a child next to your label text. The Badge base class is inline-flex items-center gap-1.5, so icons and text align and space automatically — no wrapper div, no manual gap class at the call site. Size the icon to w-3 h-3 (12 px) to match the text-xs label, and the pair fits balanced inside the badge's px-2.5 py-0.5 padding. Any icon library works — Lucide, Heroicons, or inline SVGs.
Not with a built-in asChild prop — Drivn's Badge always renders as a <span>. To make it clickable, wrap it in a Next.js <Link>, a native <button>, or the Drivn Button component. The span inherits click events naturally when nested. If you do this often enough that the wrapping feels repetitive, open src/components/ui/badge.tsx after install and change the span to an <a> tag or add a conditional render based on an href prop you introduce yourself.
Open src/components/ui/badge.tsx and add a new key to the styles.variants object — for example "info": "bg-info/15 text-info border border-info/20". The variant prop type is derived via keyof typeof styles.variants, so TypeScript autocompletes your new variant at every call site immediately. If you want the new color to also be a design token, add a matching --color-info variable in src/styles/globals.scss so dark and light themes both resolve it correctly.
Wrap both the button and the badge in a relative inline-flex container. Absolutely position the badge to the top-right corner with absolute -top-1 -right-1 and trim the padding with className="px-1.5 py-0" so single-digit counts fit cleanly. Use the destructive variant for unread counts — the red accent is a conventional attention-needed signal that most users parse without reading. For counts over 99, render 99+ as the badge child instead of the raw number.

