Tag React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React tag and chip component: toggling filter chips, removable tags, a dot, icon, avatar or count, in groups that wrap, scroll or fold.
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.
Tag is a Pro React component in the React data display components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add tag, together with Avatar, Motion presets, Navigation, Number format, Popover and 8 more and the npm packages @radix-ui/react-slot, lucide-react and motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add tagTag is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import Tag from @/components/ui/tag.
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
childrendefault"New York"ReactNodeThe label. Long labels truncate at 16rem and show the full text in a title.
variantdefault"soft""soft" | "outline" | "solid"soft is a tinted fill (a light gray for neutral), outline a hairline border, solid the full tone fill for the loudest labels.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Semantic color from the design tokens; it paints the fill, the text, the border and the dot.
colordefault""stringAny CSS color, for CRM or kanban labels. Replaces the tone; soft and outline mix it for contrast in both themes.
sizedefault"md""sm" | "md" | "lg"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.
radiusdefault"full""sm" | "md" | "full"6px, 8px or a pill.
dotdefaultfalsebooleanA leading 6px dot in the tone. An icon or an avatar takes its place when set.
selectedVariantdefault"solid""solid" | "soft"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.
showCheckdefaulttruebooleanThe check that scales in and draws itself while selected.
iconReactNodeA leading icon; unsized svgs follow the tag size.
avatarstring | { 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.
countdefault""numberA 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.
selectabledefaultfalsebooleanMakes the tag a toggle button (aria-pressed): a filter chip. For a row of chips with shared selection, use TagGroup (see Parts).
defaultSelecteddefaultfalsebooleanStarting state while uncontrolled.
selectedbooleanControlled selected state; pair it with onSelectedChange.
onSelectedChange(selected: boolean) => voidFires when a selectable tag is toggled.
onRemove() => voidShows the × button. Backspace / Delete on the focused tag call it too.
removeLabeldefault"Remove"stringThe × button's aria-label, followed by the tag's text: "Remove New York". TagGroup takes a partial labels object instead (see DEFAULT_TAG_LABELS).
textValuedefault""stringThe plain text of a label that is not a string, for the remove label and the title.
hrefdefault""stringRenders 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>) => voidMakes a plain pressable tag. On a selectable tag, preventDefault skips the toggle.
disableddefaultfalsebooleanDims the tag and blocks pressing and removing it.
asChilddefaultfalsebooleanRenders 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.
localedefault""stringIntl 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.
classNamesPartial<Record<"button" | "check" | "dot" | "icon" | "avatar" | "label" | "count" | "remove", string>>Extra classes for the inner parts.
classNamestringExtra classes for the chip, merged last. Override the tone vars here, e.g. [--tone-foreground:white].
Parts
TagGroup
componentA 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.
items*TagItem[]The tags to render (see TagItem).
selectionModedefault"none""none" | "single" | "multiple"Whether tags toggle, and how many can be selected.
valuestring[]Controlled selected values; single mode keeps at most one.
defaultValuedefault"[]"string[]Selected values at first, uncontrolled.
onValueChange(value: string[]) => voidFires with the new selection.
onRemove(value: string) => voidShows × on every item whose removable is not false.
overflowdefault"wrap""wrap" | "scroll" | "collapse"Wrap onto lines, one swipeable row with faded edges, or one row that folds extra tags into +N with selected ones pinned.
maxVisibledefault0numberCaps the visible tags before +N even when there is room; 0 means no cap.
labelstringThe group's aria-label, e.g. "Cities".
labelsPartial<TagLabels>Overrides for the +N text, its aria-label, the sheet title and the remove label.
containerHTMLElement | nullPortal target of the +N popover.
localestringIntl locale for the counts.
classNamesPartial<Record<"tag" | "more" | "popover" | "row", string>>Extra classes for the inner parts.
DEFAULT_TAG_LABELS
constantThe 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
typeOne tag of a TagGroup: { value, label, textValue?, icon?, avatar?, dot?, tone?, color?, count?, disabled?, removable?, href? }.
TagLabels
typeThe 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.
Dependencies
Files
- components/ui/tag.tsx
FAQ
Is the React Tag component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Tag?
Run npx wingo-ui@latest add tag. It also adds the Wingo UI items it builds on (Avatar, Motion presets, Navigation, Number format, Popover and 8 more) and the npm packages @radix-ui/react-slot, lucide-react and motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/tag.
How does Tag behave on phones?
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. 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.