Select React component for Tailwind CSS v4
v1.0.0Added 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 selectUsage
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*SelectItemsOptions and groups: { value, label, description?, icon?, badge?, disabled?, keywords?, textValue? } or { label, items }. keywords add search terms, textValue names a non-text label.
valuestringThe chosen option, controlled ("" is nothing). A value not among the options shows as text and highlights nothing.
defaultValuedefault""stringThe starting value while uncontrolled. A form reset returns to it.
onValueChange(value: string) => voidCalled with the new value after a pick, a typeahead jump, a clear or browser autofill.
labeldefault""ReactNodeThe field label. A click focuses the trigger (never opens it); plain text also titles the phone sheet.
descriptiondefault""ReactNodeHelper text under the field, linked with aria-describedby.
errordefault""ReactNodeReplaces the description with an animated error and marks the field invalid.
requireddefaultfalsebooleanThe red mark after the label, aria-required on the trigger and required on the hidden native select.
invaliddefaultfalsebooleanThe invalid look and aria-invalid without a message.
disableddefaultfalsebooleanDims the field, never opens and is not submitted.
readOnlydefaultfalsebooleanThe value stays visible and the field focusable, but it never opens, typeahead does nothing and there is no clear button.
placeholderdefault"Select an option"stringShown 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…"stringThe search row's placeholder.
emptyTextdefault"No results"stringShown when the search matches nothing (or there are no options).
clearabledefaultfalsebooleanAn x in the trigger while a value is set; it clears the value and focus returns to the trigger.
clearLabeldefault"Clear"stringThe clear button's accessible name and tooltip.
loadingdefaultfalsebooleanOptions are on their way: a spinner replaces the chevron and the list shows a loading row until options arrive.
loadingTextdefault"Loading…"stringThe loading row's text and the spinner's accessible name.
leftIconReactNodeA leading icon, shown while the chosen option has no icon of its own (or showSelectedIcon is off).
showSelectedIcondefaulttruebooleanThe chosen option's icon (a flag, a swatch, a plate code) in the trigger.
renderValue(option: SelectOption) => ReactNodeCustom trigger content for the chosen option; the icon and the placeholder stay.
highlightMatchesdefaulttruebooleanBold 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""stringRenders a hidden native select with this name, so FormData, required validation, form reset and autofill work.
autoCompletedefault""stringFor the hidden native select, so browser autofill can fill it ("address-level1" for a county).
idstringThe trigger's id (useId() by default); the label points at it.
fieldPropsPartial<FieldProps>Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.
defaultOpendefaultfalsebooleanStarts with the list (or the sheet) open. Pass open and onOpenChange to control it.
openbooleanWhether the list is open, controlled. It never opens while disabled or readOnly.
onOpenChange(open: boolean) => voidCalled when the list opens or closes.
responsivedefaulttruebooleanA bottom sheet with 48px rows below 768px instead of the popover.
sheetTitledefault""stringThe 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" | numberThe list's width: the field's, its content's (at least the field's), or px.
maxHeightdefault360numberThe list's maximum height in px, capped by the room on screen.
virtualThresholddefault100numberAbove this many visible options only the rows in view render.
renderOption(option, state: { selected, highlighted, disabled, ranges }) => ReactNodeReplaces a row's content; the row element, its aria and its classes stay.
containerHTMLElement | nullWhere the list or the sheet is portaled (document.body by default).
labelsPartial<{ 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.
classNamesPartial<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).
classNamestringMerged onto the Field root when there is a label, description or error, otherwise onto the box.
refRefObject<HTMLButtonElement | null> or (node) => voidThe trigger button; other props (onBlur, aria-*, data-*, form) go there too.
Parts
SelectRoot
componentOwns 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
componentThe 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
componentThe chosen option's label (or renderValue), else the placeholder. Props: { placeholder, renderValue, placeholderClassName, ...span }.
SelectAnchor
componentWhat the list aligns to when the trigger is small, e.g. the whole phone field around a flag button. Props: Slot props.
SelectContent
componentThe 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
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
Used in
More React input components
Articles using this component
- React Combobox: Async Search Without Race Conditions
Component guides10 min read
- MUI Alternatives for React and Tailwind in 2026
Comparisons and alternatives10 min read
- Best React Component Library for Mobile in 2026: 9 Compared
Comparisons and alternatives12 min read
- React Form Components: Accessible Fields, Zod and Mobile
Component guides26 min read