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

Search Input

Inputs

Search Input React component for Tailwind CSS v4

RatingSegmented Control
Live props
Field
label
description
error
required
invalid
disabled
readOnly
Content
placeholder
clearLabel
loadingLabel
defaultValue
name
resultCountLive count
Appearance
size
variant
radius
accent
State
loading
Behavior
debounce300ms
minLength0
clearable
shortcut
showShortcut
selectOnFocus
maxLengthNo limit
dropdown
recentStorageKey
maxRecent5
Layout
expandable
expandedWidth280px
Code
import { SearchInput } from "@/components/ui/search-input"

<SearchInput />
  1. Home
  2. Components
  3. Inputs
  4. Search Input
Free

Search Input React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React search input with debounced search, a focus shortcut, a spinner, a results count, recent searches and suggestions.

Press / anywhere on the page (not while typing in another field) and the search field takes focus with its text selected; the / hint at the end of the empty box shows which key it is and hides while the field is focused. Type and the search runs once you pause (300 ms by default): the magnifier crossfades into a spinner while the page loads, and a muted count like 318 results appears at the end and is read out politely. Enter searches at once, saves the query to the recent searches and closes the dropdown. On focus a dropdown opens under the field with Recent searches (each with an × to forget it and a Clear button for all of them) and suggestions, both filtered by what you type with the matches in bold. Arrow Down moves into it while the caret stays in the field, Enter picks the highlighted row, Delete or Shift+Backspace removes a highlighted recent search, and Escape first closes the dropdown, then clears the text, then leaves the field. The × in the box clears and searches at once. With expandable the field starts as a square icon button, grows with a spring on click, tap or the shortcut and folds back when you leave it empty. On a phone the keyboard shows a Search key, the text is 16px so iOS never zooms, the dropdown stays under the field above the keyboard with 44px rows, no key hint is shown, and picking a search or pressing Search lowers the keyboard so the results show.

Search Input 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 search-input, together with Field, Input, Kbd, Listbox, Spinner and 7 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 search-input

Usage

Import SearchInput from @/components/ui/search-input.

import { useState } from "react"
import { Building2, Car, Laptop } from "lucide-react"
import { SearchInput } from "@/components/ui/search-input"

export function ListingsSearch() {
  const [loading, setLoading] = useState(false)
  const [total, setTotal] = useState<number>()

  return (
    <SearchInput
      placeholder="Search listings"
      recentStorageKey="recent-listings"
      suggestions={[
        {
          label: "Categories",
          items: [
            { value: "real-estate", label: "Real estate", icon: <Building2 /> },
            { value: "cars", label: "Cars", icon: <Car /> },
            { value: "electronics", label: "Electronics", icon: <Laptop /> },
          ],
        },
      ]}
      loading={loading}
      resultCount={total}
      resultLabel={(n) => (n === 1 ? "1 listing" : `${n} listings`)}
      onSearch={async (query) => {
        if (!query) return setTotal(undefined)
        setLoading(true)
        const response = await fetch(`/api/listings?q=${encodeURIComponent(query)}`)
        const { total } = (await response.json()) as { total: number }
        setTotal(total)
        setLoading(false)
      }}
    />
  )
}

Props

  • labeldefault ""
    ReactNode

    Renders the field anatomy around the box: the label above it, linked with htmlFor. Without it the input is named by aria-label, else the placeholder.

  • descriptiondefault ""
    ReactNode

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

  • errordefault ""
    ReactNode

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

  • requireddefault false
    boolean

    The red mark after the label and the native required attribute.

  • invaliddefault false
    boolean

    The invalid look and aria-invalid without a message.

  • disableddefault false
    boolean

    Dims the field, blocks input and unbinds the shortcut.

  • readOnlydefault false
    boolean

    The query stays visible and selectable but cannot change; no clear button and no dropdown.

  • placeholderdefault "Search"
    string

    Hint text while the field is empty; also the accessible name when there is no label or aria-label. The demo passes Search listings.

  • clearLabeldefault "Clear"
    string

    The clear button's accessible name and hover title.

  • loadingLabeldefault "Searching"
    string

    What screen readers hear for the spinner.

  • defaultValuedefault ""
    string

    The starting query while uncontrolled. A form reset returns to it; pass value and onValueChange to control it instead.

  • namedefault ""
    string

    The form field name on the native input, so a GET form submits ?q=… like a plain search box.

  • labels
    Partial<{ recent: string; suggestions: string; clearRecent: string; removeRecent: string }>

    The dropdown copy, merged over { recent: "Recent searches", suggestions: "Suggestions", clearRecent: "Clear", removeRecent: "Remove" }. suggestions heads loose suggestion options and names the list for screen readers.

  • resultLabeldefault "defaultResultLabel"
    (count: number) => string

    Formats the count shown and announced; the default gives "1 result" / "318 results". The demo writes Romanian plurals (318 rezultate, 20 de rezultate).

  • icon
    ReactNode

    Replaces the leading magnifier; it still crossfades with the spinner while loading.

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

    Height 32 / 40 / 48px with 13 / 14 / 15px text (16px on phones); md grows to 44px on touch. The key hint and the count scale along.

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

    The box look, shared with every input: a filled well, a bordered box, a bottom line, or no fill until hover (toolbars).

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

    Corner shape scaled with the size like the button's; full makes the classic pill search bar.

  • accentdefault ""
    string

    Any CSS color for the focus ring, the outline and underline focus color and the caret. Empty uses the ring token.

  • loadingdefault false
    boolean

    The magnifier crossfades into a spinner and the input gets aria-busy; typing stays possible. Set it while your request runs.

  • resultCountdefault 0
    number

    Shown muted at the end of the box (dimmed while loading) and announced in a polite live region once it settles. Unset by default, which hides it; 0 shows "0 results". In the playground the slider pins a count, and Live count feeds the demo's real result count.

  • value
    string

    The query, controlled; pair it with onValueChange.

  • onValueChange
    (value: string) => void

    Fires on every keystroke, a clear and a picked suggestion.

  • onSearch
    (query: string) => void

    Fires with the trimmed query after the debounce, at once on Enter and on a picked row, and with "" at once on clear. The same query twice in a row only fires again on Enter.

  • debouncedefault 300
    number

    Milliseconds of quiet typing before onSearch runs. 0 searches on every keystroke.

  • minLengthdefault 0
    number

    Shorter queries do not call onSearch (the empty query after a clear always does), so a single letter never hits the server.

  • clearabledefault true
    boolean

    An × pops in while there is text; it clears, searches at once and keeps focus in the field. Escape clears either way.

  • shortcutdefault "/"
    string

    Focuses the field and selects its text from anywhere, in mod+k notation ("/", "mod+k", "shift+mod+f"). Keys without a modifier are ignored while you type in another field. Empty binds nothing. mod+k usually belongs to a command menu; give each search on a page its own key.

  • showShortcutdefault true
    boolean

    The key hint at the end of the box while it is empty and not focused (⌘K on Apple devices, Ctrl+K elsewhere). Never shown on touch screens.

  • selectOnFocusdefault false
    boolean

    Selects the whole query when the field takes focus by click, tap or Tab, so typing replaces it. The shortcut always selects it.

  • maxLengthdefault 0
    number

    The native character limit for the query. 0 means no limit.

  • dropdowndefault true
    boolean

    Shows recent searches and suggestions under the field while it is focused (in the page flow, not a sheet, so it stays above a phone keyboard). With it the input is a combobox driven by arrow keys; without it a plain searchbox.

  • recentStorageKeydefault ""
    string

    Keeps recent searches in localStorage under this key, read after mount. Blocked storage (private mode) falls back to memory. Empty keeps them in memory for the page's life. The demo uses its own key with three seeded searches.

  • maxRecentdefault 5
    number

    How many recent searches are kept; a repeat moves to the top instead of doubling (compared without case or diacritics).

  • recent
    string[]

    Controlled recent searches, newest first; pair it with onRecentChange (then storage is left to you).

  • onRecentChange
    (recent: string[]) => void

    Fires when a search is saved, removed, cleared, or restored from storage after mount.

  • suggestions
    ListboxItems (SearchSuggestion | SearchSuggestionGroup)[]

    Static quick picks (categories, popular searches) under the recents, filtered by the query with matches in bold. Loose options go under labels.suggestions; groups keep their label. Options take icon, description, badge (a count) and keywords.

  • onSuggestionSelect
    (option: SearchSuggestion) => void

    Called with a picked suggestion (not a recent search). Without it the option's text fills the field, searches and is saved as a recent search.

  • expandabledefault false
    boolean

    Starts as a square icon button the size of the box; a click, tap or the shortcut grows it to expandedWidth with a spring (the magnifier glides into place and the text fades in), and it folds back when it loses focus while empty.

  • expandedWidthdefault 280
    number

    Width in px once expanded, capped by the container. Only with expandable.

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

    The native change event, untouched; a clear fires it too, like a keystroke.

  • onClear
    () => void

    Called after the × or Escape emptied the field (onSearch has already run with ""). Focus stays in the input.

  • prefix
    ReactNode

    Text or an element before the query, after the magnifier: a scope like "in Cars:" or a small select. Passed to Input.

  • rightIcon
    ReactNode

    A trailing icon after the clear button, e.g. a filter or microphone glyph. Passed to Input.

  • clearIcon
    ReactNode

    Replaces the clear button's icon (a 16px X).

  • fieldProps
    Partial<FieldProps>

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

  • id
    string

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

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

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

  • classNames
    Partial<Record<InputSlot | "root" | "icon" | "shortcut" | "count" | "dropdown" | "list" | "option" | "group" | "recentRemove" | "clearRecent" | "expand", string>>

    Extra classes for the inner parts: the Input parts (box, input, clear…), the icon stack, the key hint, the count, the dropdown and its list, rows and group labels, the recent × and Clear buttons, the expand button.

  • className
    string

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

Examples

Shortcut hints

mod+k and shift+mod+f focus their field from anywhere; the hint shows ⌘K on a Mac and Ctrl+K elsewhere, and hides on focus and on touch.

Recent searches

Controlled recents: remove one with the ×, or highlight it with ↓ and press Delete; Clear history forgets them all.

Suggestions

A Categories group with icons and counts under the recents, filtered as you type; onSuggestionSelect receives the option.

Expandable in a toolbar

A phone-width header: the search starts as a round icon button, grows to 240px on tap and folds back when you leave it empty.

Sizes and looks

sm pill, outline md, lg outline pill and underline.

Loading and results count

Type and pause: the magnifier turns into a spinner for the fake request, then the count updates (318 results, 1 result).

Dependencies

motionlucide-reactfieldinputkbdlistboxspinneruse-controllable-stateuse-debounceuse-media-queryuse-shortcutmotiontext-matchui-config

Files

  • components/ui/search-input.tsx

FAQ

Is the React Search Input 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 Search Input?

Run npx wingo-ui@latest add search-input. It also adds the Wingo UI items it builds on (Field, Input, Kbd, Listbox, Spinner and 7 more) and the npm packages motion and lucide-react. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/search-input.json.

How does Search Input behave on phones?

On a phone the keyboard shows a Search key, the text is 16px so iOS never zooms, the dropdown stays under the field above the keyboard with 44px rows, no key hint is shown, and picking a search or pressing Search lowers the keyboard so the results show.

See also

InputListboxKbdCommand MenuuseDebouncehookuseShortcuthook

Used in

Marketplace Home PageLocation MapJSON ViewerLog ViewerAudit LogMembers tablePermissions MatrixToolbarChat WidgetConversation ListFAQ SectionAutomation Builder Page

and 17 more.

More React input components

SelectSignature PadTag InputTextareaTime PickerTimezone Select
All 40 React input components
PreviousRatingNext Segmented Control

Listings

30 listings around New York

/Slash
  • Schwinn city bike

    Brooklyn · Sport

    $320
  • Trek Marlin mountain bike

    Queens · Sport

    $690
  • Xiaomi electric bike

    Brooklyn · Sport

    $890
  • Kids bike, 20 inch

    Hoboken · Sport

    $110
  • iPhone 13, 128 GB, like new

    Brooklyn · Electronics

    $460
  • and 25 more listings

Searches after a 300 ms pause · no saved searches

Press /Slash anywhere, then ↓Down arrow through recent searches, ↵Enter searches, EscEscape closesTap the field for recent searches and categories