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.
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
| Prop | Type | Default | What it does |
|---|---|---|---|
variant | default | success | warning | destructive | default | The status. Only the three status variants carry color. |
render | element | — | Renders the badge as another element, such as a link: render={<a href />}. |
className | string | — | 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,warninganddestructiveuse 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
renderwith 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:
defaultis 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.