Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Tag Input

Inputs

Tag Input React component for Tailwind CSS v4

SwitchTextarea
Live props
Field
label
description
error
required
invalid
disabled
readOnly
Content
placeholder
name
Appearance
size
variant
radius
chipVariant
tone
color
Behavior
maxNo limit
maxLengthNo limit
backspace
editable
addOnBlur
addOnPaste
allowDuplicates
restrictToSuggestions
openOnFocus
clearable
Code
import { TagInput } from "@/components/ui/tag-input"

<TagInput />
  1. Home
  2. Components
  3. Inputs
  4. Tag Input
Pro

Tag Input React component for Tailwind CSS v4

v1.0.0·Added 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-input

Tag 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 ""
    ReactNode

    Renders the field anatomy around the box: the label above it, linked to the text input; the box is a group named by it.

  • descriptiondefault ""
    ReactNode

    Helper text under the box, linked with aria-describedby. At the limit it is replaced by labels.limit(max) when you pass none.

  • errordefault ""
    ReactNode

    Replaces the description with the animated error and marks the field invalid. Rejected tags show their own message here until the text changes.

  • requireddefault false
    boolean

    The red mark after the label and aria-required on the text input.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message.

  • disableddefault false
    boolean

    Dims the field, blocks typing, hides the × on chips and disables the hidden inputs.

  • readOnlydefault false
    boolean

    Chips stay visible and focusable but cannot be removed, edited or added to; no suggestions.

  • placeholderdefault "Add a tag"
    string

    Hint text while there are no tags; the input stays usable after. The demo passes Add a tag.

  • value
    string[]

    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[]) => void

    Fires with the new list after every add, remove, edit and clear.

  • suggestions
    ListboxItems (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) => string

    Normalizes typed text before validation, e.g. lowercase and a leading #. The default trims and collapses spaces, which also runs after yours.

  • labels
    Partial<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") => void

    Called 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 ""
    string

    Any CSS color for the chips; replaces the tone. Empty uses the tone.

  • maxdefault 0
    number

    Maximum 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.

  • maxLengthdefault 0
    number

    Characters 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.

  • editabledefault true
    boolean

    Double-click a chip, tap a focused chip, or press Enter or F2 on it to edit it in place. Emptying it removes the tag.

  • addOnBlurdefault true
    boolean

    Leaving the field adds what was typed (validated like Enter).

  • addOnPastedefault true
    boolean

    A pasted text with delimiters, new lines or tabs becomes several tags at once; off, it is pasted as plain text.

  • allowDuplicatesdefault false
    boolean

    Off, a tag that already exists (compared without case or diacritics) is not added and the existing chip pulses.

  • restrictToSuggestionsdefault false
    boolean

    Only suggestions can become tags: the first match is highlighted as you type and other text is rejected with labels.notInList.

  • openOnFocusdefault false
    boolean

    Opens 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.

  • clearabledefault false
    boolean

    A Clear all × at the end of the box while there are tags; focus returns to the text.

  • namedefault ""
    string

    One hidden input per tag with this name, so FormData.getAll(name) returns the list. The demo uses tags while empty.

  • fieldProps
    Partial<FieldProps>

    Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.

  • id
    string

    The text input's id. Defaults to useId(); inside a <Field> the Field's id wins.

  • ref
    RefObject<HTMLInputElement | null> or (node) => void

    The text input. Every other native prop lands there too: inputMode, enterKeyHint, autoFocus, onBlur, onKeyDown (runs first; preventDefault skips the built-in keys), aria-*, data-*.

  • classNames
    Partial<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.

  • className
    string

    Classes for the outermost element: the Field root when there is a label, description or error, else the tag input root.

Parts

  • tagValidators

    constant

    Ready-made validate functions: tagValidators.email, .url and .hashtag. Each returns false for a bad tag, so labels.invalid names it.

  • TagInputRejectReason

    type

    Why onReject fired: "invalid", "duplicate", "limit" or "notInList".

  • TagInputSuggestion

    type

    One 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.

Dependencies

motionlucide-reactbadgefieldlistboxuse-controllable-statemotiontext-matchtonesui-config

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

Multi SelectBadgeListboxInputSearch InputField

Used in

Members tableInvoice PageReferral Page

More React input components

TextareaTime PickerTimezone SelectTree SelectDuration PickerOTP Input
All 40 React input components

Articles using this component

  • React Multi Select: Search, Chips, Select All and Mobile

    Component guides · Oct 16, 2026 · 10 min read

  • React Tag Input: Chips from Typing, Paste and Suggestions

    Component guides · Oct 13, 2026 · 8 min read

PreviousSwitchNext Textarea

Post a listing

Trek FX 2 city bike, New York

Title
Trek FX 2 city bike, 2021
bikevintage

["bike","vintage"]

⌫ edit+edit+blur+paste-dupes-restrict-openOnFocusname=tagsmaxLength=∞

↵Enter or , adds, ←Left arrow moves onto the tags, ⌫Delete edits the last oneType and press Enter or comma; double-tap a tag to edit it