Tag

Interactive tag / chip primitive — Badge with an optional dismiss button. Mirrors Badge's variants so the visual sibling is obvious; ships with a close affordance for filter pills, multi-select selections, draft attachments.

Filter pills — dismissable

frontendopenpriority: 1

Multi-select assignees

AdaGraceMargaret

Status tags — non-interactive (no onRemove)

ActiveDraftArchivedBanned

Installation

pnpm
pnpm dlx @hex-core/cli add tag

Multi-select selections

Stack of selected values with per-tag dismiss

tsx
<div className="flex flex-wrap gap-2">
  {selected.map((tag) => (
    <Tag key={tag} variant="secondary" onRemove={() => deselect(tag)}>
      {tag}
    </Tag>
  ))}
</div>

Status tag (non-interactive)

Static tag without dismiss — equivalent to Badge but uses Tag for consistency

tsx
<Tag variant="outline">Beta</Tag>

Variant values

variantVisual style — mirrors Badge so users get consistent token use.
ValueDescription
defaultdefault
Primary-tinted filled tag.
secondary
Muted neutral filled tag with a hairline border.
destructive
Red-tinted filled tag.
outline
Bordered transparent tag.

API Reference

PropTypeDefaultDescription
variant
"default" | "secondary" | "destructive" | "outline"defaultVisual style.
icon
ReactNodeOptional leading icon (sized 12x12 inside the tag).
onRemove
functionClick handler for the close button. When provided, the dismiss ✕ is rendered. Signature: () => void.
removeLabel
stringOverride the auto-derived close-button aria-label (default: 'Remove ${children}').
className
stringAdditional CSS classes

AI Guidance

When to use

Use for tokens the user can dismiss: filter pills, multi-select selections, draft attachments. Pair onRemove with a state setter that drops the value from your collection.

When not to use

Don't use for non-interactive labels (use Badge). Don't use for state-bearing 'click to filter' affordances — those should be Toggle or ToggleGroup so the active state is announced as 'pressed'/'not pressed' to assistive tech.

Common mistakes

  • Forgetting onRemove on a tag the user CAN dismiss — leaves the affordance invisible
  • Calling it Tag but rendering inside a list of static labels — confuses Tag (interactive) vs Badge (decorative) semantics
  • Wrapping the tag itself in a <button> — the dismiss button already handles its own click; double-button breaks accessibility

Accessibility

Close button gets aria-label derived from children when they're a string ('Remove Urgent'). Override via removeLabel. Tag itself is a span — wrap in a list (`role='list'` + `role='listitem'`) when rendering N tags as a collection.

Token budget: 350