# Tag

> A React tag and chip component: toggling filter chips, removable tags, a dot, icon, avatar or count, in groups that wrap, scroll or fold.

- Type: React component (Data Display)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add tag`
- Docs: https://wingo-ui.com/components/tag

## Interaction

Click a selectable tag to toggle it: it fills in and a check grows in on a quick spring while the chip widens smoothly. Click × to remove a tag; in a group the others slide into its place. With the keyboard a group is one Tab stop: Left / Right (and Up / Down) move between tags, Home / End jump to the ends, Space / Enter toggle, Backspace / Delete remove the focused tag and focus moves to the next one. A lone tag has its × in the Tab order and Backspace / Delete on it removes it too. In a collapsed group the +N tag opens a popover with the hidden tags; below 768px it is a bottom sheet listing them as rows you tap to toggle or remove. While it is open the rows stay put; newly selected tags move into the row when it closes, and Escape returns focus to +N. On a phone the scroll overflow is a swipeable filter bar that fades at its edges, and every chip and × gets a 44px hit area without growing.

## Installation

```bash
npx wingo-ui@latest add tag
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/tag` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/tag.tsx`

npm dependencies: `npm i @radix-ui/react-slot lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Number format](https://wingo-ui.com/components/number-format) ([Markdown](https://wingo-ui.com/components/number-format.md))
- [Popover](https://wingo-ui.com/components/popover) ([Markdown](https://wingo-ui.com/components/popover.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useOverflowItems](https://wingo-ui.com/components/use-overflow-items) ([Markdown](https://wingo-ui.com/components/use-overflow-items.md))
- [useScrollEdges](https://wingo-ui.com/components/use-scroll-edges) ([Markdown](https://wingo-ui.com/components/use-scroll-edges.md))
- [useSelection](https://wingo-ui.com/components/use-selection) ([Markdown](https://wingo-ui.com/components/use-selection.md))

## Usage

```tsx
import { useState } from "react"
import { MapPin } from "lucide-react"
import { Tag, TagGroup, type TagItem } from "@/components/ui/tag"

const CITIES: TagItem[] = [
  { value: "new-york", label: "New York", count: 128 },
  { value: "chicago", label: "Chicago", count: 342 },
  { value: "seattle", label: "Seattle", count: 96 },
  { value: "austin", label: "Austin", count: 74 },
]

export function ListingFilters() {
  const [cities, setCities] = useState<string[]>(["new-york"])
  const [freeShipping, setFreeShipping] = useState(true)
  return (
    <div className="flex flex-col gap-3">
      <TagGroup
        label="Cities"
        items={CITIES}
        selectionMode="multiple"
        value={cities}
        onValueChange={setCities}
        overflow="scroll"
        locale="en-US"
      />
      {freeShipping && (
        <Tag icon={<MapPin />} onRemove={() => setFreeShipping(false)} removeLabel="Remove">
          Free shipping
        </Tag>
      )}
    </div>
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children?` | `ReactNode` | `"New York"` | The label. Long labels truncate at 16rem and show the full text in a title. |
| `variant?` | `"soft" \| "outline" \| "solid"` | `"soft"` | soft is a tinted fill (a light gray for neutral), outline a hairline border, solid the full tone fill for the loudest labels. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Semantic color from the design tokens; it paints the fill, the text, the border and the dot. |
| `color?` | `string` | `""` | Any CSS color, for CRM or kanban labels. Replaces the tone; soft and outline mix it for contrast in both themes. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | 24 / 28 / 32px tall, text 12 / 13 / 14px, icons 12 / 14 / 16px. On touch screens the chip keeps its look and gets a 44px hit area. |
| `radius?` | `"sm" \| "md" \| "full"` | `"full"` | 6px, 8px or a pill. |
| `dot?` | `boolean` | `false` | A leading 6px dot in the tone. An icon or an avatar takes its place when set. |
| `selectedVariant?` | `"solid" \| "soft"` | `"solid"` | The look while selected: a solid fill in the tone (black / white for neutral, like a pressed filter chip) or a toned soft fill with a border. Unselected filter chips stay neutral, so the tone marks only the selected ones. |
| `showCheck?` | `boolean` | `true` | The check that scales in and draws itself while selected. |
| `icon?` | `ReactNode` |  | A leading icon; unsized svgs follow the tag size. |
| `avatar?` | `string \| { src?: string; name: string }` |  | A leading avatar (16 / 20 / 24px). A string is a photo url; with a name the initials show while it loads. |
| `count?` | `number` | `""` | A trailing muted count ("Overdue 3"), formatted for the locale and read with the label. No count by default. In the demo an empty control shows 1,280 and any text that is not a number hides the count. |
| `selectable?` | `boolean` | `false` | Makes the tag a toggle button (aria-pressed): a filter chip. For a row of chips with shared selection, use TagGroup (see Parts). |
| `defaultSelected?` | `boolean` | `false` | Starting state while uncontrolled. |
| `selected?` | `boolean` |  | Controlled selected state; pair it with onSelectedChange. |
| `onSelectedChange?` | `(selected: boolean) => void` |  | Fires when a selectable tag is toggled. |
| `onRemove?` | `() => void` |  | Shows the × button. Backspace / Delete on the focused tag call it too. |
| `removeLabel?` | `string` | `"Remove"` | The × button's aria-label, followed by the tag's text: "Remove New York". TagGroup takes a partial labels object instead (see DEFAULT_TAG_LABELS). |
| `textValue?` | `string` | `""` | The plain text of a label that is not a string, for the remove label and the title. |
| `href?` | `string` | `""` | Renders the tag as a link through the app's router link (NavLink from lib/navigation). The demo ignores it while asChild is on. |
| `onClick?` | `(event: MouseEvent<HTMLElement>) => void` |  | Makes a plain pressable tag. On a selectable tag, preventDefault skips the toggle. |
| `disabled?` | `boolean` | `false` | Dims the tag and blocks pressing and removing it. |
| `asChild?` | `boolean` | `false` | Renders your single child (an <a>, a router Link) as the pressable part, with the check, icon and count around its content. The demo swaps in a link. |
| `locale?` | `string` | `""` | Intl locale for the count (1,280 in en-US, 1.280 in de-DE). Empty uses the UIProvider locale, then the page's. The demo passes en-US while it is empty. |
| `classNames?` | `Partial<Record<"button" \| "check" \| "dot" \| "icon" \| "avatar" \| "label" \| "count" \| "remove", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the chip, merged last. Override the tone vars here, e.g. [--tone-foreground:white]. |

## Parts

### TagGroup (component)

A row of tags from an items array with shared selection, removal and overflow. Roving focus: one Tab stop, arrow keys, Home / End, Space / Enter to toggle, Backspace / Delete to remove. In collapse mode the hidden tags open in a popover, or a bottom sheet below 768px. size, variant, tone, color, radius, selectedVariant, showCheck and disabled work as on Tag and apply to every item; an item's own tone and color win.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `TagItem[]` |  | The tags to render (see TagItem). |
| `selectionMode?` | `"none" \| "single" \| "multiple"` | `"none"` | Whether tags toggle, and how many can be selected. |
| `value?` | `string[]` |  | Controlled selected values; single mode keeps at most one. |
| `defaultValue?` | `string[]` | `"[]"` | Selected values at first, uncontrolled. |
| `onValueChange?` | `(value: string[]) => void` |  | Fires with the new selection. |
| `onRemove?` | `(value: string) => void` |  | Shows × on every item whose removable is not false. |
| `overflow?` | `"wrap" \| "scroll" \| "collapse"` | `"wrap"` | Wrap onto lines, one swipeable row with faded edges, or one row that folds extra tags into +N with selected ones pinned. |
| `maxVisible?` | `number` | `0` | Caps the visible tags before +N even when there is room; 0 means no cap. |
| `label?` | `string` |  | The group's aria-label, e.g. "Cities". |
| `labels?` | `Partial<TagLabels>` |  | Overrides for the +N text, its aria-label, the sheet title and the remove label. |
| `container?` | `HTMLElement \| null` |  | Portal target of the +N popover. |
| `locale?` | `string` |  | Intl locale for the counts. |
| `classNames?` | `Partial<Record<"tag" \| "more" \| "popover" \| "row", string>>` |  | Extra classes for the inner parts. |

### DEFAULT_TAG_LABELS (constant)

The English strings TagGroup uses: more (n => "+3"), moreLabel ("3 more: Boston, Denver, Seattle"), moreTitle ("More") and remove ("Remove"). Spread it to translate a few.

### TagItem (type)

One tag of a TagGroup: { value, label, textValue?, icon?, avatar?, dot?, tone?, color?, count?, disabled?, removable?, href? }.

### TagLabels (type)

The shape of the labels object: more, moreLabel, moreTitle and remove.

## Examples

- **Applied filters**: Removable tags with a clear-all action; the others slide into place. Backspace removes the focused one.
- **CRM labels**: Dots in status tones and custom colors, with a count read together with the label.
- **People chips**: Avatars at the start, removable like assignees, plus a pressable add tag.
- **Collapse into +N**: One row in a narrow card; the rest fold into +N, a popover on desktop and a sheet on phones.
- **Phone filter bar**: overflow="scroll": one swipeable row with snap and faded edges, single selection.
- **Single choice in a tone**: selectionMode="single" with the soft selected look in brand orange.
- **Sizes, variants and selected looks**: 24 / 28 / 32px, soft / outline / solid, the two selected looks, radii and disabled.

## See also

- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Tag Input](https://wingo-ui.com/components/tag-input) ([Markdown](https://wingo-ui.com/components/tag-input.md))
- [Toggle Group](https://wingo-ui.com/components/toggle-group) ([Markdown](https://wingo-ui.com/components/toggle-group.md))
- [Toolbar](https://wingo-ui.com/components/toolbar) ([Markdown](https://wingo-ui.com/components/toolbar.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))

---

Canonical: https://wingo-ui.com/components/tag
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
