How to use it
The live demo shows every kind of badge in rows: statuses (Approved, In Progress, a dashed Not applicable, Failed, Waiting for approval, Submitted, Pending, Archived), three removable file badges with counts (Folders 4, Files 12, Images 36) that fold away when removed while the rest slide over, then come back with a restore button, a person with an avatar and a country with its flag, and presence badges (Online and Offline with a haloed dot, Do not disturb).
When to use it
- Showing the state of a row in a table or list, such as Approved, Pending or Failed.
- Active filters or picked values that the user can remove one by one.
- Small labels for a person, a country or presence next to a name, or a link to a topic with href.
When to reach for something else
- A main action such as Save or Delete. Use a button; a clickable badge reads as a label first.
- A toggle between filters. Use a switch or a segmented control, which expose pressed or checked state.
- Long text. The label never wraps; it is cut with an ellipsis when the badge runs out of room.
Keyboard
- Tab
- Focuses the badge when it has href or onClick, then its remove button when it has onRemove. A plain badge is not focusable.
- Enter / Space
- Follows the link, clicks the badge, or clicks the focused remove button. Space does not follow a link.
Accessibility
- A plain badge is a <span>, so screen readers read its label as text in the flow.
- With href the label is an <a>; with onClick it is a <button type="button">. Its hit area covers the whole pill, and the pill shows the focus outline.
- The icon and media are aria-hidden: the label carries the meaning, so color is never the only signal. The count is read after the label.
- The remove button is a real <button type="button"> named "Remove" plus the label's text through aria-labelledby, whatever the children are, or removeLabel when set. It shows a focus outline in the ring color.
- After a remove, focus moves to the next badge's remove button or action in the same parent, or the previous one when it was the last, so keyboard users keep their place and screen readers announce where they landed.
- A label cut with an ellipsis shows its full text as a tooltip on hover.
- Under reduced motion a removed badge goes at once, with no fold.
Theming
- Each tone is only a text color: neutral is text-foreground and danger text-destructive; success is Tailwind emerald-700 (emerald-400 in dark), warning amber-700 (amber-400 in dark) and info blue-700 (blue-400 in dark), since shadcn/ui has no success, warning or info token and primary is near black in most themes. Those shades keep the text at 4.5:1 or better on the badge's tint in each theme.
- The fill is that color at 8% and the outline at 15% (30% dashed), both from currentColor, so a text-* class in className gives a new tone, for example className="text-violet-600". Hover on a clickable badge adds a little more fill.
- Because the fill comes from the text color, a tone built on a theme token follows the theme with no dark: class. A palette color needs a dark: shade, as success and warning have, to keep its contrast.
- The focus outline uses the ring token.
Edge cases
- Sizes are sm (24px), md (28px, the default) and lg (32px); text, icons, media and the close button scale with them.
- media is fitted to a circle of 16, 20 or 24px by size, as far from the rim as from the top and bottom. Whatever is passed fills it, an img or an Avatar component with its own size class alike. In icon, svg icons without a size-* class are drawn 14, 16 or 18px; anything with its own size-* class keeps it, like a small presence dot.
- The badge never wraps its label. Inside a narrow parent the label is cut with an ellipsis, and the icon, count and remove button stay whole.
- Removing folds the badge to nothing, the gap after it included, so its neighbours in a flex or grid row slide over; then onRemove is called. Take the badge out of your state there. A badge still mounted a moment later, say after a cancelled confirm or a failed request, unfolds again.
- The fold closes the gap using the parent's column-gap, so it assumes a flex or grid parent; elsewhere it folds without closing a gap.
- With href the label renders as an a, or as linkAs (Next's Link, say) for client-side navigation; target and rel go to it.
Troubleshooting
The badge folds away but leaves an empty spot.
The badge folds first, then onRemove is called. Take the badge out of the list you render in that callback; a badge left mounted unfolds again a moment later.
My color class does not change the badge.
Pass a text color (text-violet-600), not a background: the fill and outline are drawn from the text color.
The component renders with no background or the wrong colors.
Colors use the standard shadcn/ui tokens only (background, foreground, primary, secondary, muted, accent, border, input, ring, destructive), so a project set up with shadcn/ui needs nothing extra and the component follows its theme, dark mode included. Without shadcn/ui, define those CSS variables for :root and .dark and map them in your Tailwind v4 @theme, or run npx shadcn init.
Stuck on something else? Ask in the Discord.
Some of these designs take cues from interfaces we've admired around the web. If one started as your idea and you'd like credit, message us on X @devignerui or in the Discord and we'll add it.