Badge

A badge is a small label for a status. It has a neutral style and three status variants, and can be rendered as a link.

BadgeSuccessWarningFailedNew component
import { ArrowRight } from "lucide-react";
import { Badge } from "@/components/ui/badge";

export default function BadgeDemo() {
  return (
    <>
      <Badge>Badge</Badge>
      <Badge variant="success">Success</Badge>
      <Badge variant="warning">Warning</Badge>
      <Badge variant="destructive">Failed</Badge>
      <Badge render={<a href="/components" />}>
        New component
        <ArrowRight />
      </Badge>
    </>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

"use client";

import { mergeProps } from "@base-ui/react/merge-props";
import { useRender } from "@base-ui/react/use-render";
import { cx } from "@/lib/cx";
import { focusRing } from "@/lib/styles";

type Variant = "default" | "success" | "warning" | "destructive";

// Status variants use a soft wash (about 9% fill, 30% border) with a full-strength dot, as
// the theming rules ask: the full color is for the indicator, never the whole surface.
const base = [
  "inline-flex items-center gap-1.5 rounded-sm border px-2 py-0.5 text-xs font-medium whitespace-nowrap text-text",
  "[&_svg:not([class*='size-'])]:size-3 [&_svg]:shrink-0",
  "transition-colors duration-150",
  focusRing,
].join(" ");

const dot = "before:size-1.5 before:shrink-0 before:rounded-full before:content-['']";

// [a&] only matches when the badge is rendered as a link, so a plain badge has no hover state.
const variants: Record<Variant, string> = {
  default: "border-border bg-surface [a&]:hover:bg-border",
  success: `border-accent/30 bg-accent/10 ${dot} before:bg-accent [a&]:hover:bg-accent/20`,
  warning: `border-warning/30 bg-warning/10 ${dot} before:bg-warning [a&]:hover:bg-warning/20`,
  destructive: `border-danger/30 bg-danger/10 ${dot} before:bg-danger [a&]:hover:bg-danger/20`,
};

export interface BadgeProps extends useRender.ComponentProps<"span"> {
  variant?: Variant;
}

export function Badge({ variant = "default", className, render, ...props }: BadgeProps) {
  return useRender({
    defaultTagName: "span",
    render,
    props: mergeProps<"span">({ className: cx(base, variants[variant], className) }, props),
  });
}

Props

PropTypeDefaultWhat it does
variantdefault | success | warning | destructivedefaultThe status. Only the three status variants carry color.
renderelement—Renders the badge as another element, such as a link: render={<a href />}.
classNamestring—Extra classes, added after the badge's own.

Every other prop goes to the element the badge renders, a span unless you pass render.

The thinking

A badge labels something; it doesn't ask for anything. So it stays small and quiet, and only a status gets color.

  • Status, not decoration: success, warning and destructive use the accent, warning and danger tokens. Reach for them only when the label really is a status.
  • Quiet color: A soft wash and a colored dot, never a solid fill, so a row of badges doesn't shout. This is the low-emphasis pattern from Theming.
  • A link when it needs to be: Pass render with a link and the badge gains hover and focus states. Without it, a badge has none.

Interface design

  • Size: 12px medium text with 8px of side padding and 2px above and below. Corners use the sm radius, 4px.
  • Status color: A 10% fill and a 30% border in the status color, plus a 6px dot at full strength. The label stays in the main text color.
  • Neutral: default is the surface fill with a hairline border and no dot.
  • Icons: An icon goes after the text, 12px, as in the link example.

Interaction

  • A plain badge: No hover, focus or pressed state, because it isn't a control.
  • As a link: Hover deepens the fill, to 20% for a status badge. Keyboard focus draws the 2px outline.

Empathy

  • Words, not just color: The dot is color, but the label says the status. Write Failed, not only a red dot.
  • Contrast: The label is the main text color on a dark wash, well above the 4.5:1 that AA asks for.
  • Links say where they go: A badge used as a link should name its destination, like New component.