Skip to contentVibraUI
Data display

Badge

A small capsule for a count, a label or a state.

Vibra's default badge reads --brand and hovers by mixing ink into the fill rather than fading it to 80%; destructive is the danger tone on its opaque tint in both modes, and a linked one marks hover with its border instead of a deeper fill. Focus is the kit's solid accent outline, and the padding beside an icon sits on the inline axis, so the icon keeps it in a right-to-left page. For a status word with a tone per state, reach for StatusBadge; for removable tags, TagList; for a delta, MetricDelta.

Install

npx shadcn@latest add @vibra/badge

Needs the @vibra registry in your components.json — set it up once.

Examples

With an icon

data-icon="inline-start" tightens the padding on the icon's side; the word carries the meaning.

Counts

Caps at 99+ so the column keeps its width; a screen reader hears the whole figure and what it counts.

As a link

render swaps the span for an anchor and keeps the look, the focus outline included.

Beside a title

Qualifies what the title names: a feature in beta, or one the plan has to include.

In a table

A plan is a category, so it is a Badge; a status with a tone per value is StatusBadge.

Marks the default

One row marked apart from the rest; every other row offers to become it.

Pinned to a card

Across the card's top edge, labelling the card without taking a line of its content.

With a face

A person as a chip. The face is decoration beside the printed name, so it is hidden whole.

In progress

A spinner in the icon's place that stops under reduced motion; the word says what is running.

Long label

Truncates inside a span at the width it is given; the whole name stays in the text.

With an explanation

Rendered as a button so the keyboard reaches its tooltip, and described by the same sentence for a screen reader.

Announcement

An outline badge as the link, carrying a filled one for the word that matters.

Right to left

The same badges in Arabic: each icon keeps its tighter padding, and the arrow turns.

Props

PropTypeDefaultDescription
variant"default" | "secondary" | "destructive" | "outline" | "ghost" | "link""default"default is the accent fill; destructive the danger tone on its tint; outline a hairline.
renderReact.ReactElement—Renders the badge as another element — a link, say — keeping its look.

Dependencies

Source

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

// The tighter padding beside an icon sits on the inline axis (ps/pe), so it
// follows the icon to the right-hand side of a right-to-left page.
const badgeVariants = cva(
  "group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-[background-color,border-color,color] duration-(--duration-fast) ease-(--ease-standard) focus-ring has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 aria-invalid:border-destructive [&>svg]:pointer-events-none [&>svg]:size-3!",
  {
    variants: {
      variant: {
        default:
          "bg-brand text-brand-foreground [a]:hover:bg-[color-mix(in_oklch,var(--brand),var(--foreground)_10%)]",
        secondary:
          "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
        // Same fill in both modes, and a linked badge marks its hover with the
        // border it already reserves room for: deepening the tint would move
        // the background towards the text it is under.
        destructive: "bg-danger-muted text-danger [a]:hover:border-danger",
        outline:
          "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
        ghost: "hover:bg-muted hover:text-muted-foreground",
        link: "text-brand underline-offset-4 hover:underline",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function Badge({
  className,
  variant = "default",
  render,
  ...props
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
  return useRender({
    defaultTagName: "span",
    props: mergeProps<"span">(
      {
        className: cn(badgeVariants({ variant }), className),
      },
      props
    ),
    render,
    state: {
      slot: "badge",
      variant,
    },
  })
}

export { Badge, badgeVariants }