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

Combobox

Inputs

Combobox React component for Tailwind CSS v4

Color PickerCountry Select
Live props
Field
label
description
error
required
invalid
disabled
readOnly
name
Behavior
debounce250ms
minQueryLength0
fuzzy
allowCustomValue
creatable
openOnFocus
autoHighlight
clearable
virtualThreshold100
State
defaultValue
defaultInputValue
defaultOpen
Content
placeholder
emptyText
loadingText
errorText
retryLabel
clearLabel
toggleLabel
sheetTitle
cancelLabel
Appearance
highlightMatches
size
variant
radius
Layout
responsive
side
align
maxHeight360px
Code
import { Combobox } from "@/components/ui/combobox"

<Combobox />
  1. Home
  2. Components
  3. Inputs
  4. Combobox
Pro

Combobox React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React combobox and autocomplete: type to filter a list or search a server, with matches in bold, a create row and a phone search sheet.

Type and the list opens under the field with the matching letters in bold and the best matches first ("st louis" finds Saint Louis, diacritics never matter); with loadOptions the query goes to your server after a short pause, a spinner shows in the field while the previous results stay, and an old answer never overwrites a newer one. Click an option (or press Enter on the highlighted one) to fill the field and close the list. Backspace edits the chosen text and the value clears once the text no longer spells it. When you leave the field the text goes back to the chosen option, or empties; with allowCustomValue whatever you typed becomes the value. With creatable a "Create" row appears when nothing matches exactly. The chevron toggles the list and the x clears it. Keyboard: ArrowDown opens the list (every option when the field is empty) and moves down, ArrowUp moves up, Alt+ArrowDown opens without moving, PageUp / PageDown jump, Enter picks, Escape closes the list and a second Escape clears the field, Home / End move the caret, Tab closes and keeps the rule above. With openOnFocus the list opens as soon as the field gets focus. On a phone the field never raises the keyboard: a tap opens a full-height sheet with a search box (the keyboard comes up at once) and Cancel, big 48px rows, and a tap on a result fills the field and closes the sheet; the keyboard's search key picks the first result. name submits the value through a hidden input.

Combobox 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 combobox, together with Drawer, Field, Listbox, Spinner, Text match and 4 more and the npm packages @radix-ui/react-popover, 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 combobox

Combobox 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 Combobox from @/components/ui/combobox.

import { useState } from "react"
import { MapPin } from "lucide-react"
import { Combobox, type ComboboxItems } from "@/components/ui/combobox"

async function searchCities(query: string, { signal }: { signal: AbortSignal }): Promise<ComboboxItems> {
  const response = await fetch(`/api/cities?q=${encodeURIComponent(query)}`, { signal })
  const cities: { id: string; name: string; state: string }[] = await response.json()
  return cities.map((city) => ({ value: city.id, label: city.name, description: city.state }))
}

export function CityField() {
  const [city, setCity] = useState("")
  return (
    <Combobox
      label="City"
      placeholder="Search for a city"
      leftIcon={<MapPin />}
      loadOptions={searchCities}
      minQueryLength={1}
      value={city}
      onValueChange={setCity}
      emptyText="No cities found"
      errorText="Could not load cities"
      sheetTitle="Search cities"
      name="city"
    />
  )
}

Props

  • labeldefault ""
    ReactNode

    The label above the field; renders the field anatomy and names the list and the phone sheet.

  • descriptiondefault ""
    ReactNode

    Helper text under the field, linked with aria-describedby.

  • errordefault ""
    ReactNode

    Replaces the description with an animated error and marks the field invalid.

  • requireddefault false
    boolean

    A red * after the label and required on the input.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message.

  • disableddefault false
    boolean

    Dims the field and blocks typing and opening.

  • readOnlydefault false
    boolean

    The text stays visible and focusable; nothing opens and nothing can be typed.

  • optionsdefault "[]"
    ComboboxItems ((ComboboxOption | { label?: ReactNode; items: ComboboxOption[] })[])

    Options and groups, filtered locally as you type. An option is { value, label, description?, icon?, badge?, disabled?, keywords?, textValue? }. With loadOptions they show while the query is shorter than minQueryLength.

  • loadOptions
    (query: string, context: { signal: AbortSignal }) => Promise<ComboboxItems>

    Server search for the debounced query. The results replace the list, the previous ones stay while loading, a newer query aborts the older request, and the last 20 answers are cached.

  • debouncedefault 250
    number

    Milliseconds of quiet before loadOptions runs.

  • minQueryLengthdefault 0
    number

    loadOptions waits for this many characters.

  • filterdefault true
    boolean, or (option: ComboboxOption, query: string) => score (number, null hides)

    true filters with text-match on the label and keywords; false means the options are already filtered; a function scores each option (null hides it).

  • fuzzydefault false
    boolean

    Letters in order may have gaps ("sfco" finds San Francisco).

  • value
    string

    The chosen option's value (or the free text with allowCustomValue), controlled. A value that is not in the current results shows the cached option text, else the value.

  • defaultValuedefault ""
    string

    The starting value while uncontrolled. A form reset returns to it.

  • onValueChange
    (value: string) => void

    Called with the new value; "" when it clears.

  • inputValue
    string

    The text in the field, controlled.

  • defaultInputValuedefault ""
    string

    The starting text while uncontrolled; it filters the list like typed text.

  • onChange
    (event: ChangeEvent<HTMLInputElement>) => void

    The native change event of every keystroke, untouched (react-hook-form's register works).

  • onInputValueChange
    (text: string) => void

    Called on every keystroke and when the text snaps to an option.

  • allowCustomValuedefault false
    boolean

    The typed text is a valid value (a street, a free label): it becomes the value on blur or Enter. Otherwise the text snaps back to the chosen option.

  • creatabledefault false
    boolean

    A create row when nothing matches exactly (first and highlighted when there are no results).

  • onCreate
    (query: string) => (string | void | Promise<string | void>)

    Runs for the create row; a returned value (or a promise of one) is selected, and a promise shows a spinner on the row. Without it the typed text becomes the value.

  • createLabeldefault "(q) => `Create "${q}"`"
    (query: string) => ReactNode

    The create row's text.

  • placeholderdefault ""
    string

    Shown while the field is empty, also in the phone sheet's search box.

  • emptyTextdefault "No results"
    string

    Shown when the query matches nothing.

  • loadingTextdefault "Loading…"
    string

    The first load's status row and the spinner's accessible name.

  • errorTextdefault "Could not load results"
    string

    Shown when loadOptions fails, above a retry button.

  • retryLabeldefault "Try again"
    string

    The retry button under errorText.

  • clearLabeldefault "Clear"
    string

    The accessible name of the x button.

  • toggleLabeldefault "Show suggestions"
    string

    The accessible name of the chevron button.

  • highlightMatchesdefault true
    boolean

    The matched letters of each option in bold.

  • openOnFocusdefault false
    boolean

    Opens with every option (or the recent picks you pass) as soon as the field gets focus.

  • autoHighlightdefault true
    boolean

    The first match is highlighted, so Enter picks it. Turn it off for free text, where Enter should keep what you typed.

  • clearabledefault true
    boolean

    An x while there is text or a value; Escape on a closed list clears too.

  • leftIcon
    ReactNode

    An icon before the text, like a MapPin. Unsized svgs follow the size.

  • sizedefault "md"
    "sm" | "md" | "lg"

    32 / 40 / 48px like every field; md grows to 44px on touch screens.

  • variantdefault "soft"
    "soft" | "outline" | "underline" | "ghost"

    The box: a filled well, an outline, a bottom line, or nothing until hover.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    Corner shape, scaled per size like the button (md is 8 / 10 / 12px); also the sheet's search box.

  • defaultOpendefault false
    boolean

    Starts with the list (or the sheet) open. Pass open and onOpenChange to control it.

  • open
    boolean

    Whether the list is open, controlled. It shows only when there is something to show, and never while disabled or readOnly.

  • onOpenChange
    (open: boolean) => void

    Called when the list opens or closes.

  • responsivedefault true
    boolean

    Below 768px the field opens a full-height search sheet instead of the popover.

  • sheetTitledefault "Search"
    string

    The phone sheet's title for screen readers (its visible header is the search box); a string label wins.

  • cancelLabeldefault "Cancel"
    string

    The text button next to the sheet's search box.

  • sidedefault "bottom"
    "top" | "right" | "bottom" | "left"

    Where the list opens; it flips when there is no room.

  • aligndefault "start"
    "start" | "center" | "end"

    How the list lines up with the field.

  • maxHeightdefault 360
    number

    The list's height limit in px, capped by the room on screen.

  • virtualThresholddefault 100
    number

    Above this many rows only the visible ones render.

  • renderOption
    (option, state: { selected, highlighted, disabled, ranges }) => ReactNode

    Replaces a row's content; the row element, its aria and its classes stay.

  • namedefault ""
    string

    Submits the value through a hidden input.

  • id
    string

    The input's id; generated when left out.

  • container
    HTMLElement | null

    Portal target for the list and the sheet (default document.body).

  • fieldProps
    Partial<FieldProps>

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

  • classNames
    Partial<Record<"field" | "label" | "description" | "error" | "counter" | "root" | "box" | "input" | "leftIcon" | "spinner" | "clear" | "toggle" | "content" | "list" | "item" | "group" | "status" | "sheet" | "sheetSearch" | "cancel", string>>

    Extra classes for the inner parts. content is the popover, sheet the phone sheet.

  • className
    string

    Extra classes for the outermost element: the Field when there is a label or message, else the combobox root.

  • ref
    Ref<HTMLInputElement> (a useRef object or an (el) => void callback)

    The text input. Other props (onBlur, onKeyDown, autoFocus, aria-*, data-*) land on it too; onChange gets the native event.

Examples

Groups

Sync options in two groups; the list opens on focus.

Creatable

A create row adds the missing brand and selects it.

Free text

allowCustomValue: suggestions help, any street is accepted.

Failing API

The error row with a retry button; the last results stay.

Recent picks

openOnFocus shows recent searches before anything is typed.

Sizes and variants

32 / 40 / 48px boxes with a leading MapPin, soft, outline and underline.

Dependencies

@radix-ui/react-popovermotionlucide-reactdrawerfieldlistboxspinnertext-matchuse-controllable-stateuse-media-querymotionui-config

Files

  • components/ui/combobox.tsx

FAQ

Is the React Combobox component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Combobox?

Run npx wingo-ui@latest add combobox. It also adds the Wingo UI items it builds on (Drawer, Field, Listbox, Spinner, Text match and 4 more) and the npm packages @radix-ui/react-popover, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/combobox.

How does Combobox behave on phones?

Below 768px the field opens a full-height search sheet instead of the popover. The responsive prop controls this and is on by default.

See also

SelectMulti SelectTag InputListboxSearch InputField

Used in

Onboarding FlowMarketplace Home PageCheckout stepsFilter panelTimezone SelectAddress InputInvoice line itemsCRM Pipeline PageInvoice PageMarketplace Shell

More React input components

Country SelectCron PickerCurrency InputDate PickerDate Time PickerEmail Input
All 40 React input components

Articles using this component

  • React Combobox: Async Search Without Race Conditions

    Component guides · Oct 16, 2026 · 10 min read

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

    Component guides · Oct 16, 2026 · 10 min read

PreviousColor PickerNext Country Select

Shipping address

Order #48213 · express shipping

city="" · 250ms · min 0 · virtual > 100

“Searching…” · “No cities found” · “Could not load cities” → “Try again”

Type “san” or “st louis” · on phones: “Search cities” sheet with “Cancel”

↓Down arrow list EnterEnter pick EscEscape close, then clear