Components · Data display & feedback
Badge
Small, non-interactive status labels. Six variants share one geometry — only fill and text color change. Always pair a semantic badge with a text label.
Preview
Status icon legend
Success — action completed or status is healthy
Warning — attention required, not yet critical
Error — action failed or status is critical
Information — neutral notice or guidance
Neutral — no status assigned
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "Badge" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `badge` spec (variants: neutral, primary, success, warning, danger, info) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `badge` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
╭──────────────╮
│ ● Label │ ← {rounded.full}, 12px Geist, 10px × 4px padding
╰──────────────╯
↑ optional leading icon (12px, inherits textColor)
Parts:
- Container — pill surface with fill, border, and radius
- Label — 12px / 400 Geist, centered
- Leading icon (optional) — 12px, inherits
textColor, 4px gap
Badge is non-interactive. It has no hover, focus, or active states in standard use.
| State | Visual |
|---|---|
| Default | Semantic fill + text + border (see variant matrix in frontmatter) |
| Disabled | 50% opacity on fill and text — use sparingly |
For dismissible badge interaction states, see Dismissible Badge section below.
When to use
- Status labels on list rows, table cells, or card headers: "Active", "Pending", "Overdue".
- Numeric counts embedded in a surface — use
badge-dangerfor unread notifications or error counts. - Classification tags on content items (categories, priority levels).
When not to use
- Clickable tags or filters — use Tag instead.
- Long-form status labels (over 3 words) — use Lozenge for wider labels.
- Standalone numeric counters on nav items — use a notification dot or Badge positioned as an overlay on the nav icon.
Do
- Always include a text label — never use a badge with color alone.
- Match semantic variant to the status: success = completed/approved, danger = failed/critical, warning = at-risk/pending.
- Keep label text to 1–3 words maximum.
Don't
- Don't add hover, focus, or active states — badges are static.
- Don't use more than 2 badge variants in the same row — visual noise overwhelms the data.
- Don't use
badge-primary(Lime fill) for status — it's for featured/active classification, not semantic meaning.
- Label format: noun or short noun phrase. Title case. No verbs. "In Review", "Approved", "Overdue".
- Numeric badges: locale-aware formatting. Abbreviate at 1000+: "1.2k", "24k". Never show decimals on counts.
- Avoid color-only labeling: never use "Green badge" or "Red badge" in documentation — use the semantic variant name.
Keyboard
Standard badge is non-interactive. For the dismissible badge variant:
| Key | Behavior |
|---|---|
Tab |
Move focus to the remove button |
Enter / Space |
Remove the badge |
Accessibility
- ARIA: badges are typically presentational. Add
aria-labelon the badge container only when the text alone is insufficient to convey meaning (e.g. a count badge overlaid on a bell icon:aria-label="3 unread notifications"). - Color independence: in the accessibility theme,
badge-neutral-accessibilityremoves the fill and uses a white border — the text label is the only differentiator. Implementations must not rely on fill color to distinguish badge variants programmatically. - Icon:
aria-hidden="true"on leading icons — the text label carries the semantic meaning. - Contrast: all text/background pairs across all six variants and all three themes must meet 4.5:1 (WCAG AA). Verified at design-token level.
Spec
Values and token references resolved straight from the Zetta spec.
- backgroundColor
- {colors.surface-muted}
- textColor
- {colors.badge-neutral-text}
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
- borderRadius
- {rounded.full}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 400
- lineHeight
- 1.5
- paddingX
- {spacing.sm}
- paddingY
- {spacing.xs}
- backgroundColor
- transparent
- textColor
- {colors.ink}
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
- backgroundColor
- {colors.brand}
- textColor
- {colors.brand-text}
- borderColor
- rgba(0,0,0,0.15)
- borderWidth
- 1px
- borderRadius
- {rounded.full}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 400
- lineHeight
- 1.5
- paddingX
- {spacing.sm}
- paddingY
- {spacing.xs}
- backgroundColor
- {colors.success-bg}
- textColor
- {colors.success-text}
- borderColor
- {colors.success}
- borderWidth
- 1px
- borderRadius
- {rounded.full}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 400
- lineHeight
- 1.5
- paddingX
- {spacing.sm}
- paddingY
- {spacing.xs}
- backgroundColor
- {colors.warning-bg}
- textColor
- {colors.warning-text}
- borderColor
- {colors.warning}
- borderWidth
- 1px
- borderRadius
- {rounded.full}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 400
- lineHeight
- 1.5
- paddingX
- {spacing.sm}
- paddingY
- {spacing.xs}
- backgroundColor
- {colors.danger-bg}
- textColor
- {colors.danger-text}
- borderColor
- {colors.danger}
- borderWidth
- 1px
- borderRadius
- {rounded.full}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 400
- lineHeight
- 1.5
- paddingX
- {spacing.sm}
- paddingY
- {spacing.xs}
- backgroundColor
- {colors.info-bg}
- textColor
- {colors.info-text}
- borderColor
- {colors.info}
- borderWidth
- 1px
- borderRadius
- {rounded.full}
- fontFamily
- Geist
- fontSize
- 12px
- fontWeight
- 400
- lineHeight
- 1.5
- paddingX
- {spacing.sm}
- paddingY
- {spacing.xs}
- iconSize
- 10px
- marginLeft
- {spacing.xs}
- cursor
- pointer
- borderRadius
- {rounded.full}
- padding
- 2px
- display
- inline-flex
- alignItems
- center
- justifyContent
- center
- backgroundColor
- rgba(0,0,0,0.12)
Known gaps
- Interactive badge hover shift removed in accessibility theme — hover uses
{colors.hover-bg}(#ffffff) +{colors.hover-text}(#000000) uniformly.