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

Select

Inputs

Select React component for Tailwind CSS v4

Segmented ControlSignature Pad
Live props
State
defaultValue
loading
defaultOpen
Field
label
description
error
required
invalid
disabled
readOnly
name
autoComplete
Content
placeholder
searchPlaceholder
emptyText
clearLabel
loadingText
sheetTitle
Behavior
search
clearable
responsive
virtualThreshold100
Appearance
showSelectedIcon
highlightMatches
size
variant
radius
Layout
side
align
contentWidth
maxHeight360px
Code
import { Select } from "@/components/ui/select"

<Select />
  1. Home
  2. Components
  3. Inputs
  4. Select
Free

Select React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React select with search, groups, icons, virtualized rows and a hidden native select for forms.

Click the field (or tap it) to open the list under it, growing out of the field; click an option to pick it, a check draws itself next to it and the list closes. Clicking the field again, clicking outside, Escape or Tab closes it without changing anything. Above 8 options (or with search="always") a search row sits on top and takes focus: typing filters as you go, case and diacritics never matter ("canon" finds Cañon City, state codes like NY work as keywords), matched letters turn bold and groups without matches disappear. Keyboard: Tab reaches the field and a click on its label only focuses it. While closed, Enter, Space, ArrowDown or ArrowUp open it on the chosen option (ArrowUp on the last one when nothing is chosen), Home and End open it on the first and last option, and typing letters picks the next option that starts with them without opening, like a native select (the same letter again cycles). While open, ArrowDown and ArrowUp move and wrap, PageUp and PageDown jump a screen, Home and End go to the ends, Enter picks and closes, Escape or Alt+ArrowUp close and keep focus on the field. With clearable an x appears while a value is set; it clears it and focus goes back to the field. With loading a spinner replaces the chevron and the list shows a loading row. Lists above the virtual threshold render only the rows in view, so thousands of cities scroll at 60fps. On a phone the field opens a bottom sheet titled with the label, with 48px rows (a tap picks and closes), the chosen row scrolled into view, and the search row under the title (it waits for a tap so the keyboard does not cover the list, unless search is always). With name, a hidden native select submits the value, blocks an empty required field and lets browser autofill fill the state. With reduced motion the list fades instead of scaling and the check appears without drawing.

Select is a free 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 select, together with Listbox, Field, Drawer, Spinner, Text match and 4 more and the npm packages @radix-ui/react-popover, @radix-ui/react-slot, 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 select

Usage

Import Select from @/components/ui/select.

import { useState } from "react"
import { MapPin } from "lucide-react"
import { Select, type SelectOption } from "@/components/ui/select"

const STATES: SelectOption[] = [
  { value: "CA", label: "California", keywords: ["CA", "Los Angeles"] },
  { value: "NY", label: "New York", keywords: ["NY", "New York City"] },
  { value: "OR", label: "Oregon", keywords: ["OR", "Portland"] },
  { value: "TX", label: "Texas", keywords: ["TX", "Austin"] },
]

export function ShippingState() {
  const [state, setState] = useState("")
  return (
    <Select
      label="State"
      placeholder="Choose the state"
      searchPlaceholder="Search a state or its code…"
      emptyText="No state found"
      options={STATES}
      value={state}
      onValueChange={setState}
      leftIcon={<MapPin />}
      clearable
      required
      name="state"
      autoComplete="address-level1"
    />
  )
}

Props

  • options*
    SelectItems

    Options and groups: { value, label, description?, icon?, badge?, disabled?, keywords?, textValue? } or { label, items }. keywords add search terms, textValue names a non-text label.

  • value
    string

    The chosen option, controlled ("" is nothing). A value not among the options shows as text and highlights nothing.

  • defaultValuedefault ""
    string

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

  • onValueChange
    (value: string) => void

    Called with the new value after a pick, a typeahead jump, a clear or browser autofill.

  • labeldefault ""
    ReactNode

    The field label. A click focuses the trigger (never opens it); plain text also titles 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

    The red mark after the label, aria-required on the trigger and required on the hidden native select.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message.

  • disableddefault false
    boolean

    Dims the field, never opens and is not submitted.

  • readOnlydefault false
    boolean

    The value stays visible and the field focusable, but it never opens, typeahead does nothing and there is no clear button.

  • placeholderdefault "Select an option"
    string

    Shown in the trigger while nothing is chosen.

  • searchdefault "auto"
    "auto" | "always" | "never"

    The search row on top of the list: auto shows it above 8 options.

  • searchPlaceholderdefault "Search…"
    string

    The search row's placeholder.

  • emptyTextdefault "No results"
    string

    Shown when the search matches nothing (or there are no options).

  • clearabledefault false
    boolean

    An x in the trigger while a value is set; it clears the value and focus returns to the trigger.

  • clearLabeldefault "Clear"
    string

    The clear button's accessible name and tooltip.

  • loadingdefault false
    boolean

    Options are on their way: a spinner replaces the chevron and the list shows a loading row until options arrive.

  • loadingTextdefault "Loading…"
    string

    The loading row's text and the spinner's accessible name.

  • leftIcon
    ReactNode

    A leading icon, shown while the chosen option has no icon of its own (or showSelectedIcon is off).

  • showSelectedIcondefault true
    boolean

    The chosen option's icon (a flag, a swatch, a plate code) in the trigger.

  • renderValue
    (option: SelectOption) => ReactNode

    Custom trigger content for the chosen option; the icon and the placeholder stay.

  • highlightMatchesdefault true
    boolean

    Bold matched letters while searching.

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

    32 / 40 / 48px (md grows to 44px on touch), 16px text on phones.

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

    The field look, the same as every other field.

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

    Corner radius, scaled per size like the button; ignored by underline.

  • namedefault ""
    string

    Renders a hidden native select with this name, so FormData, required validation, form reset and autofill work.

  • autoCompletedefault ""
    string

    For the hidden native select, so browser autofill can fill it ("address-level1" for a county).

  • id
    string

    The trigger's id (useId() by default); the label points at it.

  • fieldProps
    Partial<FieldProps>

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

  • 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 never opens while disabled or readOnly.

  • onOpenChange
    (open: boolean) => void

    Called when the list opens or closes.

  • responsivedefault true
    boolean

    A bottom sheet with 48px rows below 768px instead of the popover.

  • sheetTitledefault ""
    string

    The phone sheet's title. Empty uses the label's text, else "Select an option".

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

  • contentWidthdefault "trigger"
    "trigger" | "auto" | number

    The list's width: the field's, its content's (at least the field's), or px.

  • maxHeightdefault 360
    number

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

  • virtualThresholddefault 100
    number

    Above this many visible options only the rows in view render.

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

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

  • container
    HTMLElement | null

    Where the list or the sheet is portaled (document.body by default).

  • labels
    Partial<{ results: (count: number) => string; clearSearch: string; retry: string }>

    Screen reader and search strings: the result count announced after a search ("12 results"), the search clear button.

  • classNames
    Partial<Record<"field" | "label" | "description" | "error" | "counter" | "box" | "trigger" | "value" | "placeholder" | "icon" | "chevron" | "clear" | "content" | "search" | "list" | "item" | "group" | "empty", string>>

    Extra classes for the inner parts. box is the field box, trigger the button inside it, content the popover (or the sheet).

  • className
    string

    Merged onto the Field root when there is a label, description or error, otherwise onto the box.

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

    The trigger button; other props (onBlur, aria-*, data-*, form) go there too.

Parts

  • SelectRoot

    component

    Owns the listbox engine and the open state and renders no DOM. Reads flags and ids from an enclosing Field. Props: { options, value, defaultValue, onValueChange, open, defaultOpen, onOpenChange, search, responsive, loading, virtualThreshold, filter, fuzzy, disabled, readOnly, required, invalid, size, id, name, autoComplete, form, children }.

  • SelectTrigger

    component

    The field box with the trigger button, clear button and chevron (and the hidden native select). asChild turns your own element into the trigger (a ghost sort button, a flag button). Props: { asChild, variant, radius, size, clearable, clearLabel, leftIcon, showSelectedIcon, loadingText, classNames, ...button }.

  • SelectValue

    component

    The chosen option's label (or renderValue), else the placeholder. Props: { placeholder, renderValue, placeholderClassName, ...span }.

  • SelectAnchor

    component

    What the list aligns to when the trigger is small, e.g. the whole phone field around a flag button. Props: Slot props.

  • SelectContent

    component

    The popover list on desktop, the bottom sheet on phones. Props: { contentWidth, maxHeight, side, align, sideOffset, container, sheetTitle, searchPlaceholder, emptyText, loadingText, highlightMatches, renderOption, labels, className, classNames }.

Examples

Groups, icons and counts

Groups with sticky labels, descriptions and counts. Search appears on its own above 8 options; a disabled row is skipped.

Virtualized

About 275 cities render virtually. Diacritics never matter: “canon” finds Cañon City, “oregon” the state.

Clearable and required

An x clears the value and returns focus to the trigger; a required field shows its error until something is picked.

Sizes, variants and radius

32, 40 and 48px, the four field looks and the radius scale, identical to every other field.

Custom value

renderValue adds the price next to the name; the option's swatch becomes the trigger icon.

Parts: a sort button

SelectRoot with SelectTrigger asChild turns a ghost button into the trigger; contentWidth auto sizes the list to its rows.

Native form

name renders a hidden native select: FormData, required validation, reset and autofill work like a real one.

Dependencies

@radix-ui/react-popover@radix-ui/react-slotmotionlucide-reactlistboxfielddrawerspinnertext-matchmotionuse-media-queryuse-controllable-stateui-config

Files

  • components/ui/select.tsx

FAQ

Is the React Select component free?

Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.

How do I install Select?

Run npx wingo-ui@latest add select. It also adds the Wingo UI items it builds on (Listbox, Field, Drawer, Spinner, Text match and 4 more) and the npm packages @radix-ui/react-popover, @radix-ui/react-slot, motion and lucide-react. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/select.json.

How does Select behave on phones?

A bottom sheet with 48px rows below 768px instead of the popover. The responsive prop controls this and is on by default.

See also

ListboxComboboxFieldDrawerRadio GroupSegmented Control

Used in

Checkout stepsModel PickerReviewsCountry SelectCron PickerCurrency InputDate Time PickerFilter BuilderPhone InputAddress InputCSV ImportInline Edit

and 40 more.

More React input components

Signature PadTag InputTextareaTime PickerTimezone SelectTree Select
All 40 React input components

Articles using this component

  • React Combobox: Async Search Without Race Conditions

    Component guides · Oct 16, 2026 · 10 min read

  • MUI Alternatives for React and Tailwind in 2026

    Comparisons and alternatives · Oct 13, 2026 · 10 min read

  • Best React Component Library for Mobile in 2026: 9 Compared

    Comparisons and alternatives · Oct 9, 2026 · 12 min read

  • React Form Components: Accessible Fields, Zod and Mobile

    Component guides · Oct 9, 2026 · 26 min read

PreviousSegmented ControlNext Signature Pad

Shipping address

Where should we send the parcel?

Chosen: nothing

50 options · search above 8 options · virtualized above 100 · matches highlighted · list as wide as the field · no clear button · the option icon in the field · on a phone: sheet “State”