Gesso
Data display

Badge

Small read-only marks for a count or an emphasis, set inside a chip, a button, or a row, in seven role colors plus a status dot.

Badge is a small mark you set inside something else, such as a count inside a chip, an emphasis beside a row title, or a short word in a button. It renders a <span>, so it nests validly inside a button, a table cell, or a line of text, and its colors are the look's, so every role variant repaints when the look changes. For a standalone label like "Beta" or "Pro", reach for a category chip instead. CircleBadge is an 8px status dot that sits beside text. Chip ships in the same @gesso/badge install and has its own page, for a value a person picked or can dismiss.

DefaultSecondaryDestructiveOutlineGhostLinkMuted

Usage

import { Badge } from '@/components/ui/badge';

<Badge variant="secondary">Draft</Badge>

Pick the variant by weight

default is the primary fill with its contrast ink, and destructive is the destructive fill, for a status that needs attention. secondary is a quiet fill, and muted is the sunken well with muted text, for a count or a detail that should recede. outline draws a border around primary text, ghost is the label with no fill, and link is primary text that underlines on hover when it is a link. Each role variant takes the look's rounded-control, so a Badge is an 8px rectangle in Washi and a pill in ICS.

The default, secondary, and destructive variants wear a tone, the hue, and take it from their variant: primary, neutral, and danger. Pass tone to change it, as in <Badge tone="success">, or tone="inherit" to take the surrounding scope. The other variants do not read a tone.

The text is text-xs and semibold, with px-2.5 and py-0.5 around it, and the label stays on one line, which makes a Badge 22px tall. A Badge has no size prop, so set a size in className.

A Badge adds no gap between its children, so put an icon inside as a child and add gap-1 to the Badge. Hide a decorative icon from assistive tech with aria-hidden.

Verified12 new

asChild hands the Badge's classes to its one child and renders that instead, so a Badge can be a link.

import Link from 'next/link';

<Badge asChild variant="outline">
  <Link href="/changelog">What is new</Link>
</Badge>

A plain Badge is a mark, not a control, so it does not change on hover. The hover fills of default, secondary, destructive, ghost, and link apply only when asChild makes the Badge a link or a button, which is what to use when it should act.

A status dot

CircleBadge is a solid 8px circle in bg-primary, with no children. It is a dot, not a count, so put the status or the number in text beside it. Recolor it with className, like bg-destructive. It is aria-hidden already, and the text next to it carries the meaning.

Active Failed Paused

Category color

color takes a category hue and draws the mark as a tinted fill, an ink label, and a hairline in that hue, with font-medium and 8px corners in every look, even one that rounds its buttons into pills. It wins over variant and tone, because a role variant repaints with the look and a category hue keeps its meaning. variant="tag" alone is the same shape with no color.

No colorHighDone
<Badge color="orange">High</Badge>

For a colored option in a table cell, use CategoryTag from Cell fields, which wraps this and adds a remove button.

Props

Badge takes every <span> attribute, plus these.

PropTypeDefaultWhat it does
variant"default" | "secondary" | "destructive" | "outline" | "ghost" | "link" | "muted" | "tag""default"The weight of the mark. tag is the category shape with no color.
tone"neutral" | "primary" | "support" | "danger" | "success" | "warning" | "info" | "inherit"By variantThe hue, for default, secondary, and destructive.
color"gray" | "red" | "orange" | "amber" | "yellow" | "green" | "blue" | "purple" | "pink"A category hue, drawn on the tag shape. Wins over variant and tone.
asChildbooleanfalseRenders its one child with the Badge's classes, instead of a <span>.

CircleBadge takes every <span> attribute except children. badgeVariants({ variant }) returns the classes, for styling something that is not a Badge.

Accessibility

  • A Badge is a plain <span> with no role, so a screen reader reads its text in the flow of the sentence, and it takes no focus. A change in a Badge, like a count going up, is not announced, so use a live region when it needs to be.
  • Color alone should not carry a status. The label says what it is, and a Badge with no text would have no name, so keep the word in the Badge.
  • CircleBadge is aria-hidden, so a screen reader skips it. The text beside it has to say what the dot says, like "Active" or "Failed".
  • With asChild on a link or a button, the Badge shows a 2px focus ring on :focus-visible, offset from its edge. A plain Badge takes no focus.
  • Badge sets no forced-colors rules. Windows high contrast replaces its fills, so a filled variant can lose its edge, and a CircleBadge, which is only a fill, can disappear. Keep the meaning in the text.
  • The contrast of each variant's text on its fill is not checked here.

When to use something else

  • Category colors, through CategoryTag, for a standalone label like "Beta", "Pro", or "Paid", and for any value that keeps its own hue in every look, like a priority that is orange at High. A category chip is the usual way to label something, and a Badge is the small mark inside it.
  • Chip for a value a person picked, an applied filter, or anything with a dismiss button. Chip takes a leading icon and an onRemove, and it ships in this install.
  • Avatar for a person, whose fallback is initials on a neutral fill.
  • Button for something that runs an action. Reach for Badge with asChild only when the mark is a small link.

Install

shadcn add @gesso/badge

This installs Chip and chip.config with it, and Badge, CircleBadge, and Chip import from @/components/ui/badge.

On this page