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.
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.
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.
Props
BadgeProps extends HTMLAttributes<HTMLSpanElement> with:
| Prop | Type | Default | Description |
|---|---|---|---|
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 |
dot | boolean | false | Status dot before the content, in the current text colour |
icon | ReactNode | — | After the dot, before the children; aria-hidden, sized 1em |
onRemove | (event: MouseEvent<HTMLButtonElement>) => void | — | Renders the remove button; the click does not bubble to the badge onClick |
removeLabel | string | 'Remove <children>' | 'Remove' | aria-label of the remove button |
children | ReactNode | — | Wrapped in badge-content, truncated with an ellipsis when a max-w-* is set in className |
className | string | — | Merged last, after the variant, size and shape classes |
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="badge"] | span | Root. Also data-variant, data-appearance, data-size, data-shape, data-removable |
[data-slot="badge-dot"] | span | Present with dot; aria-hidden |
[data-slot="badge-icon"] | span | Wraps icon; aria-hidden |
[data-slot="badge-content"] | span | The children; min-w-0 truncate |
[data-slot="badge-remove"] | button | Present with onRemove; Headless UI Button with data-hover / data-active |
[data-slot="badge-remove-icon"] | svg | The 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 attachonClickto it expecting keyboard access; wrap it in a Button or useonRemovefor the one interactive part. - The remove button is a real
<button>(Headless UIButton), keyboard operable, named byremoveLabel, with afocus-visiblering from--ringand a 24px minimum hit box. dotandiconarearia-hidden; convey the status in the text as well.- Colour alone does not carry meaning:
subtletext colours are the contrast-safe-texttwins, but pair a colour with a word.