# Badge

> Show a compact status, count, category, or piece of metadata.

Source: https://www.honestui.com/docs/components/badge

```tsx
import Link from "next/link";
import { Check as CheckIcon } from "honestui/icons";

import { Badge } from "@/components/honest-ui/ui/badge";

export function BadgeDemo() {
  return (
    <div className="grid gap-4">
      <div className="flex flex-wrap items-center gap-2">
        <Badge>Default</Badge>
        <Badge variant="neutral">Neutral</Badge>
        <Badge variant="info">Info</Badge>
        <Badge variant="success">
          <CheckIcon aria-hidden="true" />
          Ready
        </Badge>
        <Badge variant="warning">Needs review</Badge>
        <Badge variant="error">Failed</Badge>
        <Badge variant="gradient">Experimental</Badge>
      </div>
      <div className="flex flex-wrap items-center gap-2">
        <Badge size="sm">Small</Badge>
        <Badge>Default</Badge>
        <Badge size="lg">Large</Badge>
        <Badge
          render={<Link href="/docs/components/badge" />}
          variant="outline"
        >
          Badge docs
        </Badge>
      </div>
    </div>
  );
}

```

## Overview [#overview]

Use a badge to display a short status, count, category, or piece of metadata where people scan rather than read: table rows, list items, card headers, filter chips. Badges annotate other content. They are not containers for sentences, and they are not actions — when something needs to be clicked, render the badge as a real link or button so it behaves like one. See [Links and interactivity](#links-and-interactivity).

## Anatomy [#anatomy]

A badge is a small inline container with text and an optional leading icon. It renders a `span` by default, so it participates in a sentence or row without breaking line flow. Labels stay on one line by design; the component never wraps them.

## Variants [#variants]

Choose the variant by meaning, then by emphasis. `outline` and `secondary` are neutral; `success`, `warning`, `info`, `error`, and `destructive` map to states people already know from elsewhere in the product. Use one status vocabulary across your app: if `Failed` is `error` in one table, it should not be `destructive` in another.

Honest caveat about the API surface: several variants are visual aliases of each other. `info`, `accent`, and `default` share identical styles, as do `error`, `danger`, and `destructive`, and `neutral` matches `secondary`. Prefer the semantic names (`error`, not `danger`) so intent survives future restyling.

## Sizes [#sizes]

Sizes are `sm` (18 px tall, micro type), `default` (22 px), and `lg` (32 px). Use `sm` inside dense table cells and meta lines, `default` almost everywhere, and `lg` sparingly for hero-level labels. Icon size scales with the badge automatically.

## Links and interactivity [#links-and-interactivity]

By default a badge is static decoration with no keyboard presence — nothing to focus, nothing to announce beyond its text. That is correct for statuses. To make one navigable, pass Base UI's `render` prop with a real `Link`; the badge keeps its look while gaining link semantics, a visible `focus-visible` ring, and an expanded touch target of at least 44 × 44 px on coarse pointers.

## Accessibility [#accessibility]

A badge is announced as part of the surrounding text — there is no "badge" role. Write labels that make sense spoken aloud in context: `Failed` reads naturally after a row's other content; a bare colored dot reads as nothing at all. Never encode meaning in the variant color alone; pair color with text, and add an icon (`aria-hidden`) when you want a second non-color cue.

Mark decorative icons `aria-hidden="true"` so screen readers don't announce "check" before "Ready". The component disables pointer events on child SVGs but does not hide them from assistive technology for you.

Labels use theme tokens and adapt to dark mode automatically. The `gradient` variant is the exception — it uses fixed hardcoded colors — so verify its legibility in both themes before adopting it. Because labels never wrap, very long text will overflow its container rather than break the layout; shorten the label or truncate deliberately.

Badges have no loading, disabled, or validation states — they are static metadata. If a value is still loading, show a skeleton or spinner in its place instead of a placeholder badge.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;badge&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Import the following variables into your CSS file
      

      ```css
      @theme inline {
        --color-destructive-foreground: var(--destructive-foreground);
        --color-info: var(--info);
        --color-info-foreground: var(--info-foreground);
        --color-success: var(--success);
        --color-success-foreground: var(--success-foreground);
        --color-warning: var(--warning);
        --color-warning-foreground: var(--warning-foreground);
      }

      :root {
        --destructive-foreground: oklch(0.505 0.213 27.518);
        --info: oklch(0.623 0.214 259.815);
        --info-foreground: oklch(0.488 0.243 264.376);
        --success: oklch(0.696 0.17 162.48);
        --success-foreground: oklch(0.508 0.118 165.612);
        --warning: oklch(0.769 0.188 70.08);
        --warning-foreground: oklch(0.555 0.163 48.998);
      }

      .dark {
        --destructive-foreground: oklch(0.704 0.191 22.216);
        --info: oklch(0.623 0.214 259.815);
        --info-foreground: oklch(0.707 0.165 254.624);
        --success: oklch(0.696 0.17 162.48);
        --success-foreground: oklch(0.765 0.177 163.223);
        --warning: oklch(0.769 0.188 70.08);
        --warning-foreground: oklch(0.828 0.189 84.429);
      }
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/badge.tsx

```tsx
import { mergeProps } from "@base-ui-components/react/merge-props"
import { useRender } from "@base-ui-components/react/use-render"
import { cva, type VariantProps } from "class-variance-authority"

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

const badgeVariants = cva(
  "relative inline-flex shrink-0 items-center justify-center gap-[var(--hui-space-2)] whitespace-nowrap rounded-[var(--hui-radius-1)] border-0 px-[var(--hui-space-2)] py-[var(--hui-space-1)] [font-style:normal] [font-weight:var(--hui-font-weight-regular)] outline-none focus-visible:[outline:var(--hui-focus-ring)] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:text-[var(--hui-color-foreground-base-primary)] [button,a&]:cursor-pointer [button,a&]:pointer-coarse:after:absolute [button,a&]:pointer-coarse:after:size-full [button,a&]:pointer-coarse:after:min-h-11 [button,a&]:pointer-coarse:after:min-w-11",
  {
    variants: {
      variant: {
        default:
          "bg-[var(--hui-color-background-accent-primary)] text-[var(--hui-color-foreground-base-primary)]",
        destructive:
          "bg-[var(--hui-color-background-danger-primary)] text-[var(--hui-color-foreground-base-primary)]",
        outline:
          "border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-transparent text-[var(--hui-color-foreground-base-primary)]",
        secondary:
          "bg-[var(--hui-color-background-neutral-secondary)] text-[var(--hui-color-foreground-base-primary)]",
        info: "bg-[var(--hui-color-background-accent-primary)] text-[var(--hui-color-foreground-base-primary)]",
        success:
          "bg-[var(--hui-color-background-success-primary)] text-[var(--hui-color-foreground-base-primary)]",
        warning:
          "bg-[var(--hui-color-background-attention-primary)] text-[var(--hui-color-foreground-base-primary)]",
        error:
          "bg-[var(--hui-color-background-danger-primary)] text-[var(--hui-color-foreground-base-primary)]",
        neutral:
          "bg-[var(--hui-color-background-neutral-secondary)] text-[var(--hui-color-foreground-base-primary)]",
        accent:
          "bg-[var(--hui-color-background-accent-primary)] text-[var(--hui-color-foreground-base-primary)]",
        danger:
          "bg-[var(--hui-color-background-danger-primary)] text-[var(--hui-color-foreground-base-primary)]",
        gradient:
          "bg-[linear-gradient(to_right,oklch(0.5674_0.2831_312.58_/_0.2)_0%,oklch(0.5988_0.2445_29.12_/_0.2)_100%)] text-[var(--hui-color-foreground-base-primary)]",
      },
      size: {
        default:
          "h-[22px] [font-size:var(--hui-font-size-small)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] [&_svg:not([class*='size-'])]:size-[var(--hui-space-4)]",
        sm: "h-[18px] [font-size:var(--hui-font-size-micro)] [letter-spacing:var(--hui-letter-spacing-micro)] [line-height:var(--hui-line-height-micro)] [&_svg:not([class*='size-'])]:size-[10px]",
        lg: "h-[var(--hui-space-8)] [font-size:var(--hui-font-size-regular)] [letter-spacing:var(--hui-letter-spacing-regular)] [line-height:var(--hui-line-height-regular)] [&_svg:not([class*='size-'])]:size-[14px]",
        micro:
          "h-[18px] [font-size:var(--hui-font-size-micro)] [letter-spacing:var(--hui-letter-spacing-micro)] [line-height:var(--hui-line-height-micro)] [&_svg:not([class*='size-'])]:size-[10px]",
        small:
          "h-[22px] [font-size:var(--hui-font-size-small)] [letter-spacing:var(--hui-letter-spacing-small)] [line-height:var(--hui-line-height-small)] [&_svg:not([class*='size-'])]:size-[var(--hui-space-4)]",
        regular:
          "h-[var(--hui-space-8)] [font-size:var(--hui-font-size-regular)] [letter-spacing:var(--hui-letter-spacing-regular)] [line-height:var(--hui-line-height-regular)] [&_svg:not([class*='size-'])]:size-[14px]",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

interface BadgeProps extends useRender.ComponentProps<"span"> {
  variant?: VariantProps<typeof badgeVariants>["variant"]
  size?: VariantProps<typeof badgeVariants>["size"]
}

function Badge({ className, variant, size, render, ...props }: BadgeProps) {
  const defaultProps = {
    "data-slot": "badge",
    className: cn(badgeVariants({ variant, size, className })),
  }

  return useRender({
    defaultTagName: "span",
    render,
    props: mergeProps<"span">(defaultProps, props),
  })
}

export { Badge, badgeVariants }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

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

```tsx
<Badge>Badge</Badge>
```

Pick the label first, then the least emphatic variant that still communicates the state. A quiet `outline` badge keeps attention on the content it annotates.

## Don't do this [#dont-do-this]

### Status conveyed by color alone [#status-conveyed-by-color-alone]

```tsx
// Bad
<Badge variant="error" aria-label="Failed">
  ●
</Badge>
```

```tsx
// Good
<Badge variant="error">
  <CircleXIcon aria-hidden="true" />
  Failed
</Badge>
```

A colored dot fails colorblind users, forced-colors themes, grayscale printing, and screen readers all at once — `aria-label` on a span helps only the last group. The word plus an icon gives every channel the same signal.

### Sentences inside badges [#sentences-inside-badges]

```tsx
// Bad
<Badge variant="warning">
  This invoice is overdue by more than 30 days and may incur late fees
</Badge>
```

```tsx
// Good
<div>
  <Badge variant="warning">Overdue 30 days</Badge>
  <p className="text-muted-foreground">
    Late fees apply after day 30.
  </p>
</div>
```

Labels never wrap, so long badges either overflow their container or force the surrounding layout wide enough to scroll sideways. Keep one to three words in the badge and put explanation in adjacent body text.

### Fake links [#fake-links]

```tsx
// Bad
<Badge onClick={() => router.push("/releases")}>v2.4.0</Badge>
```

```tsx
// Good
<Badge render={<Link href="/releases" />} variant="outline">
  v2.4.0
</Badge>
```

An `onClick` on a span produces something that looks clickable but cannot be focused by keyboard, opened in a new tab, or recognized as a link by assistive technology. The `render` prop outputs a real anchor with badge styling, plus the focus ring and enlarged touch target described above.

## Examples [#examples]

### Release status [#release-status]

Icon, text, and variant working together; each state is distinguishable without color.

```tsx
import { Sparkles as SparklesIcon } from "honestui/icons"

import { Badge } from "@/components/honest-ui/ui/badge"

export function BadgeReleaseStatus() {
  return (
    <div className="flex flex-wrap items-center gap-2">
      <Badge variant="secondary">
        <SparklesIcon />
        New
      </Badge>
      <Badge variant="success">Live</Badge>
      <Badge variant="outline">v2.4.0</Badge>
    </div>
  )
}

```

### Icon plus text [#icon-plus-text]

```tsx
import { Check as CheckIcon } from "honestui/icons"

import { Badge } from "@/components/honest-ui/ui/badge"

export function BadgeWithIcon() {
  return (
    <Badge variant="outline">
      <CheckIcon />
      Verified
    </Badge>
  )
}

```

### Small [#small]

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

export function BadgeSm() {
  return <Badge size="sm">Badge</Badge>
}

```

### Large [#large]

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

export function BadgeLg() {
  return <Badge size="lg">Badge</Badge>
}

```

### Link composition [#link-composition]

```tsx
import Link from "next/link"

import { Badge } from "@/components/honest-ui/ui/badge"

export function BadgeWithLink() {
  return <Badge render={<Link href="/" />}>Badge</Badge>
}

```

## API reference [#api-reference]

`Badge` accepts native `span` props and Base UI's `render` composition prop. Honest UI adds:

| Prop      | Values                                                                                                                             | Default   |
| --------- | ---------------------------------------------------------------------------------------------------------------------------------- | --------- |
| `variant` | `default`, `destructive`, `outline`, `secondary`, `info`, `success`, `warning`, `error`, `neutral`, `accent`, `danger`, `gradient` | `default` |
| `size`    | `sm`, `default`, `lg` (aliases: `micro`, `small`, `regular`)                                                                       | `default` |

Note that `info`/`accent`, `error`/`danger`/`destructive`, and `neutral`/`secondary` are style-identical aliases. The `size` aliases map `micro`→`sm`, `small`→`default`, and `regular`→`lg`.

When composing a badge into a link or button through `render`, supply a clear accessible name from the visible text; the built-in focus ring and coarse-pointer hit-area expansion apply automatically once the rendered element is an anchor or button.
