Skip to content
Drivn logoDrivn
5 min read

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.

Badges are the smallest surface in a React interface — a one- or two-word label that carries outsized meaning. A New pill draws attention to a just-released feature; a Beta marks a still-unstable page; a numeric counter reveals unread notifications; a status pill tells you a deployment is live, failed, or pending. Used well, badges are a typographic signal the eye picks up before any body copy.

Drivn's Badge component ships five variants — default, secondary, success, outline, destructive — plus automatic icon spacing via inline-flex items-center gap-1.5. It is a single <span> with no sub-components, no BadgeLabel or BadgeIcon wrapper, and no runtime dependencies beyond React and Tailwind. Pass variant, pass children (text, icons, or both), and you are done.

This page collects five badge patterns you will actually ship: a variant showcase for visual reference, an icon-plus-label pattern for active and failed states, a count badge overlayed on an icon button for notifications, a status badge used inside a data table cell, and a dynamic badge driven from a server-side field. Every snippet is copy-paste and assumes you have installed Drivn via the CLI and imported Badge from @/components/ui/badge. For an engineering comparison against shadcn/ui, see 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.

1import { Check, CircleAlert, Clock } from 'lucide-react'
2import { 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>

Notification count on an icon button

Notification counts — the red "3" on the bell icon in an app header — are one of the most common badge patterns. Wrap the Button and the badge in a relative inline-flex container, absolutely position the badge to the top-right with -top-1 -right-1, and trim the padding with px-1.5 py-0 so single-digit counts fit cleanly.

Because Drivn's Badge uses rounded-full and inline-flex items-center at the base level, a single-digit count renders as a tight circle without any extra className. For counts above 99, render 99+ as the child instead of the raw number so the badge width stays bounded. The destructive variant is the conventional choice — red for unread is a universally-parsed signal, so users register the count without reading.

1import { Bell } from 'lucide-react'
2import { Button } from '@/components/ui/button'
3import { Badge } from '@/components/ui/badge'
4
5<div className="relative inline-flex">
6 <Button
7 variant="outline"
8 size="sm"
9 className="w-9 p-0"
10 aria-label="Notifications"
11 >
12 <Bell className="w-4 h-4" />
13 </Button>
14 <Badge
15 variant="destructive"
16 className="absolute -top-1 -right-1 px-1.5 py-0"
17 >
18 3
19 </Badge>
20</div>

Status badge in a data table cell

Data tables that list orders, deployments, users, or invoices almost always want a status column with color-coded pills. Put a Badge in the cell renderer with the variant mapped to the status value — success for healthy or completed, destructive for failed or error, outline for neutral draft states, secondary for pending or in-progress.

The data table component's cell renderer receives the row data, so the mapping logic lives in a small helper object that returns the correct variant for each status string. Because the Badge uses text-xs by default, the row height stays aligned with surrounding cells without any vertical-alignment tweak. The pattern scales identically whether the table has ten rows or ten thousand — one component, one mapping, one pill per cell.

1type Status = 'active' | 'pending' | 'failed' | 'archived'
2
3const 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.

1const 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
7export function UserPlanBadge({
8 plan,
9}: { plan: keyof typeof tiers }) {
10 const { label, variant } = tiers[plan]
11 return <Badge variant={variant}>{label}</Badge>
12}
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

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.