Tag
Removable and selectable chips — dismissable filter tokens and pick-to-toggle option chips, with TagGroup coordinating roving-keyboard selection and removal across a set.
@garn/ui/tagDefault
Plain chips — neutral, brand, and a leading-icon chip.
Tone × appearance
Every tone rendered against every appearance. Hover a cell for the data attributes it emits; click the copy icon for that combination's JSX.
| solid | soft | outline | framed | |
|---|---|---|---|---|
| neutral | Design | Design | Design | Design |
| brand | Design | Design | Design | Design |
| info | Design | Design | Design | Design |
| success | Design | Design | Design | Design |
| warning | Design | Design | Design | Design |
| danger | Design | Design | Design | Design |
| discovery | Design | Design | Design | Design |
Removable
Standalone dismissable filter tokens (onRemove).
Count
A trailing count (a composed Counter) and a custom trailing slot.
Shapes
sharp · rounded · pill.
Sizes
sm · md.
Single Select
A single-select TagGroup (disallowEmptySelection) — selected reads as a check + fill.
Multi Select
A multi-select TagGroup — toggle several chips.
Removable Group
Applied-filters: a TagGroup with onRemove and an empty state.
Selectable Removable
Selectable AND removable chips in one group — both affordances on one TagGroup.
Disabled
disabledKeys — a chip that can't be selected and is skipped by roving.
Truncate
A long label truncated with maxWidth + a native tooltip.
Link
asChild — a chip that is a link (a tag that navigates).
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add tag- Registry
tag- Source
- tag.tsx
- Deps
class-variance-authority@radix-ui/react-slotlucide-react- Registry deps
utilsuse-controllable-statecounter
Import
import { Tag } from "@garn/ui/tag";Props
Generated from the component's TypeScript types.
appearance- Description
- No description.
- Type
"framed" | "outline" | "soft" | "solid"- Default
soft
shape- Description
- No description.
- Type
"pill" | "rounded" | "sharp"- Default
rounded
size- Description
- No description.
- Type
"md" | "sm"- Default
md
tone- Description
- No description.
- Type
"brand" | "danger" | "discovery" | "info" | "neutral" | "success" | "warning"- Default
neutral
asChild- Description
- Render as the child element (a link/button chip), merging props.
- Type
boolean
count- Description
- A trailing count, rendered as a Counter matched to the chip's tone/size (its appearance inverts for contrast).
- Type
number
disabled- Description
- No description.
- Type
boolean
leading- Description
- Leading content — icon, avatar, or dot.
- Type
React.ReactNode
maxWidth- Description
- No description.
- Type
number | string
onRemove- Description
- Standalone removal handler (in a group, the group's `onRemove` is used).
- Type
(value?: string) => void
removeLabel- Description
- Accessible label for the remove button. Default: `Remove {label}`.
- Type
string
trailing- Description
- Trailing content — rendered after the label, before any remove button.
- Type
React.ReactNode
truncate- Description
- Truncate a long label with an ellipsis + native tooltip.
- Type
boolean
value- Description
- Stable key for selection/removal. Required inside a selectable/removable group.
- Type
string
Plus 278 inherited native <span> attributes.
TagGroup
appearance- Description
- No description.
- Type
TagAppearance
defaultSelectedKeys- Description
- Uncontrolled initial selection.
- Type
string[]
disabledKeys- Description
- Disable these chips by `value` (in addition to a chip's own `disabled`).
- Type
string[]
disallowEmptySelection- Description
- Keep at least one chip selected (single mode can't toggle the last off).
- Type
boolean
empty- Description
- Rendered when the group has no chips (e.g. all removed).
- Type
React.ReactNode
label- Description
- Convenience group label (auto-wired as the group's accessible name).
- Type
React.ReactNode
onRemove- Description
- Enable removal: called with the removed chip's `value`.
- Type
(value: string) => void
onSelectionChange- Description
- No description.
- Type
(keys: string[]) => void
selectedKeys- Description
- Controlled selection (chip `value`s).
- Type
string[]
selectionMode- Description
- "none" (default), "single", or "multiple".
- Type
TagSelectionMode
shape- Description
- No description.
- Type
TagShape
size- Description
- No description.
- Type
TagSize
tone- Description
- Ambient defaults inherited by child Tags (each Tag may override).
- Type
TagTone
Plus 279 inherited native HTML attributes.
Styling
Target these data-slots and remap these tokens to restyle without forking the component.
[data-slot="tag"][data-slot="tag-leading"][data-slot="tag-label"][data-slot="tag-trailing"][data-slot="tag-remove"]| Property | Token | Tier |
|---|---|---|
fill | --garn-badge-brand-bgbrandable | semantic |
Private instance vars (never externalize): --t-soft-bg
Overview
Tag is the interactive chip — a keyword, label, or filter the user can remove or select, often in a TagGroup. It shares the chip family's tone × appearance × size vocabulary with Badge, but where Badge is a read-only marker, Tag carries affordances: a remove control, selected state, focus. Use it for editable label sets and filter chips; for a status you only display, use Badge, and for a single value the user picks from a menu, use BadgeSelect.
Guidelines
Use Tag when the chip is actionable. If users add, remove, or toggle it, it's a Tag — not a Badge. Make the remove control a real, labelled button ("Remove <name>") so it's reachable by keyboard and announced.
Group related tags and keep labels tight. A TagGroup communicates that the chips belong to one set (the filters on a list, the labels on an item); keep each label to a word or two so a row of them stays scannable.
Keep tone meaningful and removal forgiving. Reuse the chip tones consistently, and don't lean on color alone. When removing a tag has consequences, make it easy to undo rather than gating each removal behind a confirm.
Best practices
- Give every TagGroup an accessible name (label / aria-label).
- Set a stable
valueon each Tag in a selectable or removable group — selection and removal key off it. - Let onRemove be the source of the remove affordance (don't hand-place a remove button); pass onRemove on the group for per-chip removal, or on a standalone Tag.
- Use tone for intent and share it across the badge family (Badge / Counter / Tag) so a status reads the same everywhere.
- Use asChild to make a chip a link (a tag that filters/navigates).
- Don't use a Tag for a static label that can't be removed or selected — that's Badge.
- Don't nest your own interactive element inside a clickable standalone Tag (use a TagGroup, whose role=row hosts the remove button safely).
- Don't strip the focus ring or override the roving tabindex — they are the keyboard contract.
- Don't reach for raw colors; tone resolves through the --garn-badge-* tokens (edit dark mode in the token layer).
Content guidelines
- Keep chip labels short; enable
truncate(+ maxWidth) for user-generated tags so a long label stays a chip. - Prefer a leading icon/avatar over color to distinguish chip kinds.
Accessibility
- Role
grid- Focus
- 2px ring in --garn-ring on the chip (the roving row) and the remove button; roving tabindex keeps a TagGroup a single tab stop.
Enforceable contracts
Give the TagGroup an accessible name (a `label`, or aria-label / aria-labelledby).
when a TagGroup has no `label`, `aria-label`, or `aria-labelledby`
- • A standalone Tag has no implicit role (a <span>), becomes a <button> when clickable, or the asChild element (e.g. an <a>); keep a readable text label and don't encode meaning by color alone.
- • A TagGroup is a role=grid with one roving tab stop; each Tag is a role=row (aria-selected in single/multiple mode) wrapping role=gridcell content and a gridcell remove button — the grid pattern is what lets a chip carry a focusable remove button without a nested-interactive violation.
- • Keyboard: arrows + Home/End rove between chips (logical, so they mirror under RTL); Enter/Space toggle selection; Backspace/Delete remove the focused chip and move focus to its neighbour (or the previous chip if it was last, or the group if none remain).
- • Every remove button is named from its chip ('Remove {label}', overridable via removeLabel). The group is multiselectable only in 'multiple' mode (aria-multiselectable).
- • Give the TagGroup an accessible name via `label` (auto-wired as aria-labelledby), `aria-label`, or `aria-labelledby` — a group of chips without a name is anonymous to assistive tech.
- Status
- Stable
- Version
v0.3.0- Introduced
v0.1.0
garn doesn't yet record a per-release version history. This reflects the component's current lifecycle state; full changelog entries will appear once the metadata pipeline emits a lifecycle.history field.