# Badge

> Status badge with seven variants, optional dot, removable chip and three sizes.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/badge
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/badge.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

```bash
npm install lucide-react
```

## Usage

```tsx title="components/badge-demo.tsx"
"use client"

import * as React from "react"

import { Badge, type BadgeVariant } from "@/components/badge"

const variants: BadgeVariant[] = ["default", "secondary", "outline", "success", "warning", "destructive", "info"]

export default function BadgeDemo() {
  const [tags, setTags] = React.useState(["React", "TypeScript", "Tailwind", "Next.js"])

  return (
    <div className="flex w-full max-w-lg flex-col gap-6">
      <div className="flex flex-wrap items-center gap-2">
        {variants.map((v) => (
          <Badge key={v} variant={v} className="capitalize">
            {v}
          </Badge>
        ))}
      </div>
      <div className="flex flex-wrap items-center gap-2">
        <Badge variant="success" dot>Online</Badge>
        <Badge variant="warning" dot>Degraded</Badge>
        <Badge variant="destructive" dot>Offline</Badge>
        <Badge variant="info" dot>Syncing</Badge>
        <Badge variant="outline" dot>Draft</Badge>
      </div>
      <div className="flex flex-wrap items-center gap-2">
        <Badge size="sm" variant="secondary">Small</Badge>
        <Badge size="md" variant="secondary">Medium</Badge>
        <Badge size="lg" variant="secondary">Large</Badge>
      </div>
      <div className="flex flex-wrap items-center gap-2">
        {tags.map((tag) => (
          <Badge
            key={tag}
            variant="secondary"
            removeLabel={`Remove ${tag}`}
            onRemove={() => setTags((t) => t.filter((x) => x !== tag))}
          >
            {tag}
          </Badge>
        ))}
        {tags.length === 0 && (
          <button
            type="button"
            onClick={() => setTags(["React", "TypeScript", "Tailwind", "Next.js"])}
            className="rounded-md text-xs text-muted-foreground underline underline-offset-4 hover:text-foreground focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40"
          >
            Reset tags
          </button>
        )}
      </div>
    </div>
  )
}
```

## Source

```tsx title="components/badge.tsx"
"use client"

import * as React from "react"
import { X } from "lucide-react"

import { cn } from "@/lib/utils"

export type BadgeVariant = "default" | "secondary" | "outline" | "success" | "warning" | "destructive" | "info"
export type BadgeSize = "sm" | "md" | "lg"

export interface BadgeProps extends React.ComponentProps<"span"> {
  variant?: BadgeVariant
  size?: BadgeSize
  /** Leading status dot */
  dot?: boolean
  /** Shows an x button that calls this handler */
  onRemove?: () => void
  removeLabel?: string
}

const variantClasses: Record<BadgeVariant, { box: string; dot: string }> = {
  default: { box: "border-transparent bg-primary text-primary-foreground", dot: "bg-primary-foreground" },
  secondary: { box: "border-transparent bg-secondary text-secondary-foreground", dot: "bg-muted-foreground" },
  outline: { box: "border-border bg-transparent text-foreground", dot: "bg-muted-foreground" },
  success: {
    box: "border-emerald-500/25 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300",
    dot: "bg-emerald-500",
  },
  warning: {
    box: "border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
    dot: "bg-amber-500",
  },
  destructive: {
    box: "border-destructive/25 bg-destructive/10 text-destructive",
    dot: "bg-destructive",
  },
  info: { box: "border-sky-500/25 bg-sky-500/10 text-sky-700 dark:text-sky-300", dot: "bg-sky-500" },
}

const sizeClasses: Record<BadgeSize, string> = {
  sm: "h-5 gap-1 px-2 text-[11px]",
  md: "h-6 gap-1.5 px-2.5 text-xs",
  lg: "h-7 gap-1.5 px-3 text-sm",
}

export function Badge({
  variant = "default",
  size = "md",
  dot = false,
  onRemove,
  removeLabel = "Remove",
  className,
  children,
  ...props
}: BadgeProps) {
  const v = variantClasses[variant]
  return (
    <span
      className={cn(
        "inline-flex w-fit shrink-0 items-center whitespace-nowrap rounded-full border font-medium",
        sizeClasses[size],
        v.box,
        className
      )}
      {...props}
    >
      {dot && <span aria-hidden className={cn("size-1.5 rounded-full", v.dot)} />}
      {children}
      {onRemove && (
        <button
          type="button"
          aria-label={removeLabel}
          onClick={onRemove}
          className={cn(
            "-mr-1 flex shrink-0 items-center justify-center rounded-full opacity-70 transition hover:bg-foreground/15 hover:opacity-100 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-ring/40",
            size === "sm" ? "size-3.5" : size === "md" ? "size-4" : "size-5"
          )}
        >
          <X className={size === "lg" ? "size-3.5" : "size-3"} />
        </button>
      )}
    </span>
  )
}
```
