# Badge

> A small label pill for statuses, filters, people and presence, tinted from one text color, with sizes, an optional icon and count, link or button behavior, and a remove button that folds it away.

- Page: https://ui.devigner.cc/components/badge
- Category: Primitives
- Requirements: React 18 or 19, Tailwind CSS v4
- Install: `npx devignerui add badge` (writes components/ui/badge.tsx)
- Packages it needs: cn, motion, @devigner-ui/icons, devignerui

## What the demo shows

The live demo shows every kind of badge in rows: statuses (Approved, In Progress, a dashed Not applicable, Failed, Waiting for approval, Submitted, Pending, Archived), three removable file badges with counts (Folders 4, Files 12, Images 36) that fold away when removed while the rest slide over, then come back with a restore button, a person with an avatar and a country with its flag, and presence badges (Online and Offline with a haloed dot, Do not disturb).

## When to use it

- Showing the state of a row in a table or list, such as Approved, Pending or Failed.
- Active filters or picked values that the user can remove one by one.
- Small labels for a person, a country or presence next to a name, or a link to a topic with href.

## When to reach for something else

- A main action such as Save or Delete. Use a button; a clickable badge reads as a label first.
- A toggle between filters. Use a switch or a segmented control, which expose pressed or checked state.
- Long text. The label never wraps; it is cut with an ellipsis when the badge runs out of room.

## Usage

```tsx
import { Badge } from "@/components/ui/badge";

<Badge tone="success" icon={<IconTickCircle />}>Approved</Badge>
```

Custom composition:

```tsx
<Badge icon={<IconFolder />} onRemove={() => remove("folders")} className="text-violet-600">Folders</Badge>
```

### Removable filters

A row of filters the user clears one by one.

```tsx
import { useState } from "react";
import { IconFolder } from "@devigner-ui/icons";
import { Badge } from "@/components/ui/badge";

export function Filters() {
  const [filters, setFilters] = useState(["Folders", "Files", "Images"]);
  return (
    <div className="flex flex-wrap gap-2">
      {filters.map((f) => (
        <Badge
          key={f}
          icon={<IconFolder />}
          onRemove={() => setFilters((all) => all.filter((x) => x !== f))}
        >
          {f}
        </Badge>
      ))}
    </div>
  );
}
```

### Person with an avatar

Any image or avatar in the media slot is drawn round and sized to the badge.

```tsx
import { Badge } from "@/components/ui/badge";

export function Assignee({ name, avatar }: { name: string; avatar: string }) {
  return <Badge media={<img src={avatar} alt="" />}>{name}</Badge>;
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| tone | `"neutral" \| "success" \| "warning" \| "danger" \| "info"` | neutral | Text color of the badge; its fill and outline are the same color, faint. Any text-* class overrides it with a color of your own. |
| size | `"sm" \| "md" \| "lg"` | md | Height 24, 28 or 32px. Text, icons, media and the close button scale with it. |
| icon | `ReactNode` | - | Leading glyph, such as an icon or a status dot. An svg without a size class gets the badge's icon size. |
| media | `ReactNode` | - | Leading avatar, flag or logo, fitted to a circle sized to the badge. An img or an Avatar component both work. |
| count | `ReactNode` | - | A small count after the label, like the number of files. |
| dashed | `boolean` | false | Dashed outline and no fill, for an empty or not-applicable value. |
| href | `string` | - | Makes the whole badge a link. |
| target | `string` | - | Passed to the link, such as "_blank" for a new tab. |
| rel | `string` | - | Passed to the link, such as "noopener noreferrer". |
| linkAs | `ElementType` | a | Component that renders the link, such as Next's Link for client-side navigation. Gets href, target, rel and className. |
| onClick | `(event) => void` | - | Makes the whole badge a button. |
| onRemove | `() => void` | - | Shows a close button after the label. The badge folds away, then this is called; remove the badge in it. A badge still mounted a moment later unfolds again. |
| removeLabel | `string` | Remove <label> | Accessible name of the close button. By default it is Remove plus the label's text, whatever the children are. |

## Keyboard

| Keys | Action |
| --- | --- |
| Tab | Focuses the badge when it has href or onClick, then its remove button when it has onRemove. A plain badge is not focusable. |
| Enter / Space | Follows the link, clicks the badge, or clicks the focused remove button. Space does not follow a link. |

## Accessibility

- A plain badge is a <span>, so screen readers read its label as text in the flow.
- With href the label is an <a>; with onClick it is a <button type="button">. Its hit area covers the whole pill, and the pill shows the focus outline.
- The icon and media are aria-hidden: the label carries the meaning, so color is never the only signal. The count is read after the label.
- The remove button is a real <button type="button"> named "Remove" plus the label's text through aria-labelledby, whatever the children are, or removeLabel when set. It shows a focus outline in the ring color.
- After a remove, focus moves to the next badge's remove button or action in the same parent, or the previous one when it was the last, so keyboard users keep their place and screen readers announce where they landed.
- A label cut with an ellipsis shows its full text as a tooltip on hover.
- Under reduced motion a removed badge goes at once, with no fold.

## Theming

- Each tone is only a text color: neutral is text-foreground and danger text-destructive; success is Tailwind emerald-700 (emerald-400 in dark), warning amber-700 (amber-400 in dark) and info blue-700 (blue-400 in dark), since shadcn/ui has no success, warning or info token and primary is near black in most themes. Those shades keep the text at 4.5:1 or better on the badge's tint in each theme.
- The fill is that color at 8% and the outline at 15% (30% dashed), both from currentColor, so a text-* class in className gives a new tone, for example className="text-violet-600". Hover on a clickable badge adds a little more fill.
- Because the fill comes from the text color, a tone built on a theme token follows the theme with no dark: class. A palette color needs a dark: shade, as success and warning have, to keep its contrast.
- The focus outline uses the ring token.

## Edge cases

- Sizes are sm (24px), md (28px, the default) and lg (32px); text, icons, media and the close button scale with them.
- media is fitted to a circle of 16, 20 or 24px by size, as far from the rim as from the top and bottom. Whatever is passed fills it, an img or an Avatar component with its own size class alike. In icon, svg icons without a size-* class are drawn 14, 16 or 18px; anything with its own size-* class keeps it, like a small presence dot.
- The badge never wraps its label. Inside a narrow parent the label is cut with an ellipsis, and the icon, count and remove button stay whole.
- Removing folds the badge to nothing, the gap after it included, so its neighbours in a flex or grid row slide over; then onRemove is called. Take the badge out of your state there. A badge still mounted a moment later, say after a cancelled confirm or a failed request, unfolds again.
- The fold closes the gap using the parent's column-gap, so it assumes a flex or grid parent; elsewhere it folds without closing a gap.
- With href the label renders as an a, or as linkAs (Next's Link, say) for client-side navigation; target and rel go to it.

## Troubleshooting

**The badge folds away but leaves an empty spot.**
The badge folds first, then onRemove is called. Take the badge out of the list you render in that callback; a badge left mounted unfolds again a moment later.

**My color class does not change the badge.**
Pass a text color (text-violet-600), not a background: the fill and outline are drawn from the text color.

**The component renders with no background or the wrong colors.**
Colors use the standard shadcn/ui tokens only (background, foreground, primary, secondary, muted, accent, border, input, ring, destructive), so a project set up with shadcn/ui needs nothing extra and the component follows its theme, dark mode included. Without shadcn/ui, define those CSS variables for :root and .dark and map them in your Tailwind v4 @theme, or run npx shadcn init.
