Skip to main content

Badge

A badge is a text-height <span>: a status, a count, a tag. It is never a control on its own; onRemove adds a real <button> beside the content and that button is the only interactive part.

import Badge from '@zuilib/components/badge'

Variants and appearance

solid paints the variant colour as the surface. subtle tints the surface at 10% and writes the text in the hue's -text twin (--primary-text, --destructive-text, --success-text, --warning-text), the colour made legible as small text on --background. info reads the --primary hue (--info is a surface token, not a colour) and stays distinct only through data-variant.

Loading example
Loading example

Sizes, shape, dot and icon

Badges are labels, so the scale stops at md. Height comes from the --text-xs / --text-sm line-height; paddings are --spacing multiples. dot draws a status circle in the current text colour; icon is decorative (aria-hidden) and sized 1em.

Loading example

Removable

onRemove renders a remove button after the content. Its visible circle is text-height; an after pseudo-element grows the hit box to at least 24px. The click does not bubble to the badge's own onClick, so a selectable chip with a remove button gets one action per click. removeLabel defaults to Remove <children> when the children are plain text.

Loading example

Props

BadgeProps extends HTMLAttributes<HTMLSpanElement> with:

PropTypeDefaultDescription
variant'primary' | 'secondary' | 'outline' | 'destructive' | 'success' | 'warning' | 'info' | 'muted''primary'info reads the --primary hue; distinct through data-variant
appearance'solid' | 'subtle''solid'subtle is a 10% tint under the hue's -text colour; secondary / muted tint at 60%; outline quietens its border and text
size'sm' | 'md''md'
shape'rounded' | 'pill''rounded'--radius-md or --radius-full
dotbooleanfalseStatus dot before the content, in the current text colour
iconReactNodeAfter the dot, before the children; aria-hidden, sized 1em
onRemove(event: MouseEvent<HTMLButtonElement>) => voidRenders the remove button; the click does not bubble to the badge onClick
removeLabelstring'Remove <children>' | 'Remove'aria-label of the remove button
childrenReactNodeWrapped in badge-content, truncated with an ellipsis when a max-w-* is set in className
classNamestringMerged last, after the variant, size and shape classes

Slots

SlotElementNotes
[data-slot="badge"]spanRoot. Also data-variant, data-appearance, data-size, data-shape, data-removable
[data-slot="badge-dot"]spanPresent with dot; aria-hidden
[data-slot="badge-icon"]spanWraps icon; aria-hidden
[data-slot="badge-content"]spanThe children; min-w-0 truncate
[data-slot="badge-remove"]buttonPresent with onRemove; Headless UI Button with data-hover / data-active
[data-slot="badge-remove-icon"]svgThe X inside the remove button
/* Consumer CSS: give the info variant its own hue */
[data-slot="badge"][data-variant="info"] {
background-color: var(--info);
color: var(--info-foreground);
}

Accessibility

  • The root is a plain <span> with no role; a badge is read as text in flow. Do not attach onClick to it expecting keyboard access; wrap it in a Button or use onRemove for the one interactive part.
  • The remove button is a real <button> (Headless UI Button), keyboard operable, named by removeLabel, with a focus-visible ring from --ring and a 24px minimum hit box.
  • dot and icon are aria-hidden; convey the status in the text as well.
  • Colour alone does not carry meaning: subtle text colours are the contrast-safe -text twins, but pair a colour with a word.
  • Avatar: a status dot on a person.
  • Alert: the same variant hues on a full-width message.
  • Theming: the -text twins and how subtle picks them up.