Next.js Badge — Server-Rendered, Zero JS
Add a Next.js Badge that renders on the server with zero JavaScript. Drivn ships five hook-free variants for status, count, and label pills in pure Tailwind.
Badges are the smallest component in most design systems and one of the most repeated — a status pill on every table row, a count on every nav item, a "New" label on every release note. In a Next.js App Router project those surfaces are usually server components, and a badge gives you no reason to leave the server: it holds no state, listens to no events, and renders nothing but a styled <span>. Shipping client JavaScript for it, or pulling a runtime UI package into the bundle to draw a pill, is pure overhead.
Drivn's Badge is built to stay on the server. The entire component is one function: a styles object holding the base classes and five variants, an interface with three props, and a <span> that merges everything through cn. There is no 'use client' directive, no hook, and no browser API anywhere in the file, so Next.js renders it as static HTML and streams it with the rest of the page.
This guide installs Drivn in a Next.js 16 project, renders a Badge inside a server component, tours the five variants, maps server data to variants for status columns, and customizes the variant styles. Every snippet matches the source the CLI installs. For the full API see the Badge docs; for the head-to-head see Drivn vs shadcn/ui Badge.
Install in a Next.js 16 project
Drivn installs through a CLI that writes component source files directly into your repository — there is no runtime npm package and nothing to keep upgrading. From the root of your Next.js 16 project run npx drivn add badge. The CLI prompts once for an install directory, defaulting to src/components/ui/, then writes badge.tsx into place. The Badge carries zero peer dependencies: it imports only React and the local cn class-merge helper, so package.json does not change and there is no icon library to add. The CLI reference documents every flag, including custom install paths and adding several components in one command. Once the file lands you own it — future Drivn releases never overwrite it, and any edit you make is permanent.
1 # from the root of your Next.js 16 project 2 npx drivn add badge
Render inside a Server Component without 'use client'
App Router pages are server components by default, and any client hook inside one fails the build. Drivn's Badge has nothing that could fail — it is a pure function with no 'use client' directive, no state, and no effect, so it renders directly inside a server page and Next.js ships the markup as static HTML with zero JavaScript for the badge itself. Pass a variant to pick one of the five styles and any children for the label. The installation guide covers project setup; the Badge docs list every prop with its type and default.
1 // app/page.tsx — server component 2 import { Badge } from '@/components/ui/badge' 3 4 export default function Page() { 5 return ( 6 <div className="flex items-center gap-3 py-6"> 7 <h1 className="text-2xl font-bold">Releases</h1> 8 <Badge variant="success">Live</Badge> 9 </div> 10 ) 11 }
Five variants for status, counts, and labels
The variant prop accepts five values — default, secondary, success, outline, and destructive — each mapped to a Tailwind string in the component's styles.variants object. Four of them follow the same recipe: a token color at 15% opacity for the background, the full-strength token for the text, and a 20–30% border of the same hue. That recipe works because Drivn's colors are HSL custom properties, so opacity modifiers like bg-primary/15 resolve at the CSS level without extra tokens. The outline variant skips the tinted background entirely and reads as a quiet label. Use success for healthy states, destructive for failures, secondary for counts, and outline for metadata like version numbers. The Badge examples show all five in real layouts.
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>
Map server data to variants
Most badges in a real app are driven by data — an order status, a deployment state, a subscription tier — fetched on the server and rendered in a table or list. Because the Badge is server-safe, the lookup from data to variant can live right next to the query, with no client boundary in between. Define a record that maps each status value to a variant name and index into it at render time. The variant prop is typed as keyof typeof styles.variants, so TypeScript verifies every value in the map against the variants that actually exist in your copy of the component — add a variant, and the map type-checks against it immediately.
1 // app/orders/page.tsx — server component 2 import { Badge } from '@/components/ui/badge' 3 4 const statusVariant = { 5 paid: 'success', 6 pending: 'secondary', 7 refunded: 'outline', 8 failed: 'destructive', 9 } as const 10 11 export default async function OrdersPage() { 12 const orders = await getOrders() 13 return ( 14 <ul> 15 {orders.map((order) => ( 16 <li key={order.id} className="flex items-center gap-2"> 17 <span>{order.id}</span> 18 <Badge variant={statusVariant[order.status]}> 19 {order.status} 20 </Badge> 21 </li> 22 ))} 23 </ul> 24 ) 25 }
Customize the variants in the styles object
Because the CLI copies the source into your repo, restyling badges is a string edit, not a theme override API. The whole visual surface lives in one styles object: base holds the pill shape — inline-flex, gap-1.5 between children, rounded-full — and variants holds the five color recipes. Add a warning variant by inserting one more key with the same opacity pattern, and the keyof typeof prop type autocompletes it across the codebase instantly. To reshape every badge at once — say square corners via rounded-md — edit base. The variant colors read the primary, secondary, success, and destructive tokens, so a rebrand through the theming page restyles every badge without touching this file.
1 // src/components/ui/badge.tsx — the styles object 2 const styles = { 3 base: cn( 4 'inline-flex items-center gap-1.5 px-2.5 py-0.5', 5 'text-xs font-semibold rounded-full' 6 ), 7 variants: { 8 default: 'bg-primary/15 text-primary-light border border-primary/20', 9 secondary: 'bg-secondary/15 text-secondary border border-secondary/30', 10 success: 'bg-success/15 text-success border border-success/20', 11 outline: 'border border-border text-muted-foreground', 12 destructive: 'bg-destructive/15 text-destructive border border-destructive/20', 13 }, 14 }
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
No. The Badge is a pure function that renders a single styled <span> — it holds no state, runs no effect, and touches no browser API, so there is no 'use client' directive in the file. It renders inside any App Router page, layout, or server component, and the HTML that reaches the browser already contains the finished pill with zero JavaScript attached.
Open src/components/ui/badge.tsx after install and add a key to the styles.variants object — for example a warning entry following the same recipe of a token background at 15% opacity, full-strength text, and a soft border. The variant prop is typed as keyof typeof styles.variants, so TypeScript autocompletes the new name at every call site the moment you save the file, with no other change anywhere.
Yes. The children prop accepts any React node, and the base styles already include gap-1.5, so an icon and a label space themselves automatically. Render a lucide-react icon sized around w-3 h-3 before the text, or a tiny colored <span> dot for status pills. Keep icons decorative — the text carries the meaning for screen readers.
None. The component imports React and the local cn class-merge helper and nothing else — no Radix primitive, no cva, no icon package. The CLI writes one self-contained .tsx file of roughly thirty lines into your project, so your bundle grows by that file alone, and on server-rendered pages even that never ships to the browser.

