Skip to content
Drivn
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.

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.

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

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.

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.