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
Multi-select assignees
Status tags — non-interactive (no onRemove)
Installation
pnpm dlx @hex-core/cli add tagMulti-select selections
Stack of selected values with per-tag dismiss
<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
<Tag variant="outline">Beta</Tag>Variant values
variantVisual style — mirrors Badge so users get consistent token use.| Value | Description |
|---|---|
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
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "secondary" | "destructive" | "outline" | default | Visual style. |
icon | ReactNode | — | Optional leading icon (sized 12x12 inside the tag). |
onRemove | function | — | Click handler for the close button. When provided, the dismiss ✕ is rendered. Signature: () => void. |
removeLabel | string | — | Override the auto-derived close-button aria-label (default: 'Remove ${children}'). |
className | string | — | Additional 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.
Related components
Token budget: 350
Verified against @hex-core/components@1.9.0