Tag Input React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React tag input that turns typed, pasted or suggested text into removable chips, for tags, keywords and email recipients.
Type a word and press Enter, a comma or a semicolon and it becomes a chip that slides in, with the caret moving along after it. Paste a list like emma@acme.com, michael@acme.com; olivia@acme.com and every entry becomes its own chip, duplicates once. A word that fails validation stays in the field, the text shakes and the reason shows as the field error until you change it; typing a tag that already exists makes the existing chip pulse instead of adding it again. With suggestions a list opens under the field as you type, matches in bold and diacritics ignored: Arrow Down and Up move through it, Enter picks the highlighted one, Escape closes it. Backspace in the empty field puts the last tag back as text to edit it (select mode highlights it first and a second Backspace removes it; remove mode deletes it at once). Arrow Left at the start of the text moves to the chips: Left and Right walk between them, Home and End jump, Delete or Backspace removes the focused chip and focuses its neighbour, Enter or F2 (or a double-click, or a second tap on a focused chip) edits it in place where Enter saves and Escape cancels, and Escape or Right past the last chip returns to the text. Cmd/Ctrl+Enter adds the text and submits the form. Every chip has an × to remove it and clearable adds a Clear all button; additions and removals are read out politely. At the maximum the text field hides and the description says the limit is reached. On a phone the text is 16px so iOS never zooms, the keyboard shows an Enter key (Done at the limit), commas typed on Android keyboards still split tags, the × buttons have a large touch area and the suggestions stay in the page under the field, above the keyboard.
Tag Input is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add tag-input, together with Badge, Field, Listbox, useControllableState, Motion presets and 3 more and the npm packages motion and lucide-react.
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 tag-inputTag Input 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 TagInput from @/components/ui/tag-input.
import { useState } from "react"
import { TagInput } from "@/components/ui/tag-input"
export function ListingTags() {
const [tags, setTags] = useState(["bike", "vintage"])
return (
<TagInput
label="Listing tags"
description="Up to 8 tags, separated by commas."
placeholder="Type and press Enter"
value={tags}
onValueChange={setTags}
max={8}
name="tags"
suggestions={[
{ value: "bike", label: "bike" },
{ value: "vintage", label: "vintage" },
{ value: "trek", label: "trek" },
{ value: "city", label: "city" },
{ value: "sport", label: "sport" },
]}
/>
)
}Props
labeldefault""ReactNodeRenders the field anatomy around the box: the label above it, linked to the text input; the box is a group named by it.
descriptiondefault""ReactNodeHelper text under the box, linked with aria-describedby. At the limit it is replaced by labels.limit(max) when you pass none.
errordefault""ReactNodeReplaces the description with the animated error and marks the field invalid. Rejected tags show their own message here until the text changes.
requireddefaultfalsebooleanThe red mark after the label and aria-required on the text input.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message.
disableddefaultfalsebooleanDims the field, blocks typing, hides the × on chips and disables the hidden inputs.
readOnlydefaultfalsebooleanChips stay visible and focusable but cannot be removed, edited or added to; no suggestions.
placeholderdefault"Add a tag"stringHint text while there are no tags; the input stays usable after. The demo passes Add a tag.
valuestring[]The tags, controlled; pair it with onValueChange. A tag that equals a suggestion's value shows that option's label and icon.
defaultValuedefault"[]"string[]The starting tags while uncontrolled; a form reset returns to them.
onValueChange(tags: string[]) => voidFires with the new list after every add, remove, edit and clear.
suggestionsListboxItems (TagInputSuggestion | TagInputSuggestionGroup)[]Autocomplete options filtered while typing (case and diacritics ignored, groups kept). Options already added leave the list. A picked option adds its value; typing an option's label exactly adds its value too.
delimitersdefault"[",", ";"]"string[]Characters that add the current text as a tag, detected in the text itself so Android keyboards work. Enter always adds.
validate(tag: string, tags: string[]) => (string | boolean)Runs after transform. false rejects with labels.invalid(tag), a string rejects with that message; the text stays in the field and shakes. tagValidators.email, .url and .hashtag are exported; with the email one the keyboard switches to email.
transformdefault"trim + collapse spaces"(tag: string) => stringNormalizes typed text before validation, e.g. lowercase and a leading #. The default trims and collapses spaces, which also runs after yours.
labelsPartial<TagInputLabels>Screen reader copy and messages, merged over the English defaults: tag(tag) the chip's name ("Tag bike, press Delete to remove"), remove(tag), added(tag), removed(tag), duplicate(tag), limit(max), invalid(tag), notInList(tag), clear ("Clear all"), suggestions ("Suggestions").
onReject(tag: string, reason: "invalid" | "duplicate" | "limit" | "notInList") => voidCalled for every tag that was turned away, e.g. to show your own message when the control sits inside a <Field>.
sizedefault"md""sm" | "md" | "lg"Minimum height 32 / 40 / 48px (md 44px on touch) with 20 / 24 / 28px chips and 13 / 14 / 15px text (16px on phones). The box grows as chips wrap.
variantdefault"soft""soft" | "outline" | "underline" | "ghost"The box look shared with every input.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Box corners scaled with the size; chips follow (pill chips in a full box, square chips with none, 6px otherwise).
chipVariantdefault"soft""soft" | "outline" | "solid"The Badge variant of the chips.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Semantic color of the chips.
colordefault""stringAny CSS color for the chips; replaces the tone. Empty uses the tone.
maxdefault0numberMaximum number of tags. At the limit the text field hides, the keyboard shows Done and the description reads labels.limit(max). 0 means no limit.
maxLengthdefault0numberCharacters per tag; longer text is cut as you type or paste. 0 means no limit.
backspacedefault"edit""edit" | "select" | "remove"Backspace in the empty field: edit puts the last tag back as text, select highlights it first and a second press removes it, remove deletes it at once.
editabledefaulttruebooleanDouble-click a chip, tap a focused chip, or press Enter or F2 on it to edit it in place. Emptying it removes the tag.
addOnBlurdefaulttruebooleanLeaving the field adds what was typed (validated like Enter).
addOnPastedefaulttruebooleanA pasted text with delimiters, new lines or tabs becomes several tags at once; off, it is pasted as plain text.
allowDuplicatesdefaultfalsebooleanOff, a tag that already exists (compared without case or diacritics) is not added and the existing chip pulses.
restrictToSuggestionsdefaultfalsebooleanOnly suggestions can become tags: the first match is highlighted as you type and other text is rejected with labels.notInList.
openOnFocusdefaultfalsebooleanOpens the suggestions as soon as the field is focused instead of when you type or press Arrow Down, and keeps them open after a pick.
clearabledefaultfalsebooleanA Clear all × at the end of the box while there are tags; focus returns to the text.
namedefault""stringOne hidden input per tag with this name, so FormData.getAll(name) returns the list. The demo uses tags while empty.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.
idstringThe text input's id. Defaults to useId(); inside a <Field> the Field's id wins.
refRefObject<HTMLInputElement | null> or (node) => voidThe text input. Every other native prop lands there too: inputMode, enterKeyHint, autoFocus, onBlur, onKeyDown (runs first; preventDefault skips the built-in keys), aria-*, data-*.
classNamesPartial<Record<"field" | "label" | "description" | "error" | "counter" | "root" | "box" | "tag" | "editor" | "input" | "clear" | "suggestions" | "list" | "option", string>>Extra classes for the parts: the field anatomy, the box, each chip, the in-place editor, the text input, the clear button and the suggestions panel, list and rows.
classNamestringClasses for the outermost element: the Field root when there is a label, description or error, else the tag input root.
Parts
tagValidators
constantReady-made validate functions: tagValidators.email, .url and .hashtag. Each returns false for a bad tag, so labels.invalid names it.
TagInputRejectReason
typeWhy onReject fired: "invalid", "duplicate", "limit" or "notInList".
TagInputSuggestion
typeOne suggestion row: the Listbox option shape { value, label, description?, icon?, badge?, disabled?, keywords? }.
Examples
Emails
Email validation, paste a list, clear all and form submit with FormData.getAll.
Backspace modes
edit, select and remove, side by side.
Only from the list
restrictToSuggestions with suggestions open on focus.
Limit
max={5} with the count in the description; the input hides at the limit.
Chip styles
Sizes, box variants, chip variants, tones and a custom color.
Hashtags
transform lowercases, strips spaces and adds #; the hashtag validator.
States
Invalid, read only and disabled.
Files
- components/ui/tag-input.tsx
FAQ
Is the React Tag Input component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Tag Input?
Run npx wingo-ui@latest add tag-input. It also adds the Wingo UI items it builds on (Badge, Field, Listbox, useControllableState, Motion presets and 3 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/tag-input.
How does Tag Input behave on phones?
On a phone the text is 16px so iOS never zooms, the keyboard shows an Enter key (Done at the limit), commas typed on Android keyboards still split tags, the × buttons have a large touch area and the suggestions stay in the page under the field, above the keyboard.
See also
Used in
More React input components
Articles using this component
- React Multi Select: Search, Chips, Select All and Mobile
Component guides10 min read
- React Tag Input: Chips from Typing, Paste and Suggestions
Component guides8 min read