# Global Search

> A React global search bar for an app header: grouped results from every entity as you type, scopes, recents and suggestions, full screen on phones.

- Type: React component (Navigation)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add global-search`
- Docs: https://wingo-ui.com/components/global-search

## Interaction

Press / (or click the field) and it grows from its resting width on a smooth spring; with the query still empty the panel shows your recent searches and quick suggestions. Type and results arrive grouped (clients, contacts, offers, invoices) with the matched letters bold, a spinner in the field while the server answers and a “See all 24” row closing each long group. Arrow keys move, Enter opens the highlighted result or, with nothing highlighted, searches everywhere (the footer row does the same). Tab and Shift+Tab switch the scope chips while the panel is open; Escape closes the panel, a second Escape clears the text and a third leaves the field. Recents remember submitted queries and opened results; remove one with its × or Delete, or clear them all. On a phone the search is an icon (or a full-width field) that opens a full-screen search with the keyboard already up, 48px rows, sideways-scrolling scopes (with a keyboard, Tab reaches them and the arrow keys switch) and a Cancel button; Escape closes it.

## Installation

```bash
npx wingo-ui@latest add global-search
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/global-search` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/global-search.tsx`

npm dependencies: `npm i @radix-ui/react-dialog @radix-ui/react-popover @radix-ui/react-visually-hidden lucide-react motion`

Wingo UI dependencies (installed automatically):

- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Field](https://wingo-ui.com/components/field) ([Markdown](https://wingo-ui.com/components/field.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))
- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useDebounce](https://wingo-ui.com/components/use-debounce) ([Markdown](https://wingo-ui.com/components/use-debounce.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))

## Usage

```tsx
"use client"

import { Building2, FileWarning, Receipt } from "lucide-react"
import { GlobalSearch, type SearchResultGroup } from "@/components/ui/global-search"

type Hit = { id: string; name: string; city: string }

export function HeaderSearch() {
  return (
    <GlobalSearch
      placeholder="Search Acme CRM"
      scopes={[
        { value: "all", label: "All" },
        { value: "clients", label: "Clients", icon: <Building2 /> },
        { value: "invoices", label: "Invoices", icon: <Receipt /> },
      ]}
      loadResults={async (query, { signal, scope }): Promise<SearchResultGroup[]> => {
        const res = await fetch(`/api/search?q=${encodeURIComponent(query)}&scope=${scope}`, { signal })
        const data: { clients: Hit[]; total: number } = await res.json()
        return [
          {
            id: "clients",
            label: "Clients",
            total: data.total,
            href: `/clients?q=${encodeURIComponent(query)}`,
            items: data.clients.map((c) => ({ value: c.id, label: c.name, description: c.city, href: `/clients/${c.id}` })),
          },
        ]
      }}
      suggestions={[{ value: "overdue", label: "Overdue invoices", icon: <FileWarning />, href: "/invoices/overdue" }]}
      onSubmit={(query) => console.log("search everywhere:", query)}
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `results?` | `SearchResultGroup[]` |  | Static results filtered locally as you type (label, textValue, keywords and a text description; case and diacritics never matter). A group is { id?, label, items, total?, href? }; a result is a listbox option { value, label, description?, icon?, badge?, keywords? } plus href?, external? and meta? (a trailing amount or date). |
| `loadResults?` | `(query: string, context: { signal: AbortSignal; scope: string }) => Promise<SearchResultGroup[]>` |  | Server search, used instead of results: debounced, the previous request aborted, the last 20 answers cached, the old results kept while the new ones load; a rejected promise shows an error with Try again. A group with href and total > items.length ends in a See all row. |
| `suggestions?` | `SearchResult[]` |  | Quick links under the recents while the query is empty (“Overdue invoices”, “New clients this month”). |
| `scopes?` | `SearchScope[]` |  | Chips above the results ({ value, label, icon? }): loadResults gets the scope; static results narrow to the group whose id equals it (any other value, like “all”, keeps every group). |
| `placeholder?` | `string` | `"Search"` | The field's placeholder and accessible name, also on the phone trigger. |
| `labels?` | `Partial<GlobalSearchLabels>` |  | Every string: title, recent, suggestions, seeAll(total, label), searchFor(query, scope?), cancel, clearRecent, removeRecent, noResults(query), results(count, formatted), scopeHint, scopes, searchIn(scope), navigate, open, close, clear, loading, error, retry, minQuery(min), idle. English defaults. |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | The field box: 32 / 40 / 48px (md is 44px on touch). |
| `variant?` | `"soft" \| "outline" \| "ghost"` | `"soft"` | The field look: a filled well, an outlined box, or bare until hovered. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Corner radius of the field, scaled with its size like the button's. |
| `width?` | `number` | `320` | Resting width of the desktop field in px (capped by its container). |
| `expandedWidth?` | `number` | `480` | Width while focused or open, and the minimum width of the results panel. The field grows to it on a spring. |
| `mobileTrigger?` | `"icon" \| "field"` | `"icon"` | Phones: a 44px search icon for a bar, or a full-width field-looking button; both open the full-screen search. |
| `showShortcut?` | `boolean` | `true` | The key hint at the end of the field while it is empty and not focused (never on touch). |
| `highlightMatches?` | `boolean` | `true` | Matched letters of string labels in bold. |
| `defaultOpen?` | `boolean` | `false` | Starts with the panel (desktop) or the full-screen search (phones) open. Use open and onOpenChange to control it. |
| `open?` | `boolean` |  | Controlled open state of the panel or overlay; pair it with onOpenChange. |
| `onOpenChange?` | `(open: boolean) => void` |  | Fires when the panel or overlay opens or closes. |
| `value?` | `string` |  | Controlled query; defaultValue (default "") and onValueChange complete the pair. |
| `defaultValue?` | `string` | `""` | The query it starts with while uncontrolled. |
| `onValueChange?` | `(value: string) => void` |  | Fires on every keystroke, and when a recent query is picked or the text is cleared. |
| `scope?` | `string` |  | Controlled scope (a scope value); defaultScope (default: the first scope) and onScopeChange complete the pair. |
| `defaultScope?` | `string` | `""` | The scope it starts in while uncontrolled; empty means the first scope. The demo's scopes are all (the first), clients, contacts, proposals and invoices. |
| `onScopeChange?` | `(scope: string) => void` |  | Fires when a chip is picked or Tab cycles the scope. |
| `disabled?` | `boolean` | `false` | Dims the field, ignores input and the shortcut. |
| `onSubmit?` | `(query: string, scope: string) => void` |  | Enter with nothing highlighted, or the “Search for … everywhere” row: go to your full results page. |
| `onSelect?` | `(result: SearchResult) => void` |  | A result or suggestion was picked. Default: navigate to result.href through the LinkProvider's router (external links in a new tab). |
| `debounce?` | `number` | `250` | Milliseconds of quiet typing before loadResults runs. |
| `minQueryLength?` | `number` | `1` | Characters needed before results are searched; shorter queries show a hint and the recents stay hidden. |
| `shortcut?` | `string` | `"/"` | Focuses the field from anywhere (opens the full-screen search on phones), never while typing in another field. "" binds nothing. The showcase binds “/” itself, so the demo uses S while this is “/”. |
| `recent?` | `boolean` | `true` | Remembers submitted queries and opened results, shown while the query is empty (a repeat moves to the top). |
| `recentKey?` | `string` | `"global-search:recent"` | Local storage key for the recents (use-persisted-state), synced across tabs. "" keeps them in memory. |
| `maxRecent?` | `number` | `5` | How many recents to keep. |
| `defaultRecent?` | `string[]` |  | Recent queries shown while nothing is stored yet (first visit, onboarding). |
| `onRecentChange?` | `(recent: GlobalSearchRecent[]) => void` |  | Fires when a recent is added, removed or cleared, e.g. to sync them to the user's account. |
| `responsive?` | `boolean` | `true` | Below 768px the field becomes the phone trigger and opens a full-screen search. |
| `mode?` | `"auto" \| "popover" \| "overlay"` | `"auto"` | auto follows the screen (with responsive); popover keeps the field and its panel everywhere; overlay always uses the phone trigger and the full-screen search. |
| `locale?` | `string` | `""` | Formats the result counts in the footer and the announcements (“1,240 results”). Empty: the LocaleProvider's, else the browser's. |
| `container?` | `HTMLElement \| null` |  | Portal target of the panel and the full-screen search. Default document.body. |
| `classNames?` | `Partial<Record<GlobalSearchSlot, string>>` |  | Classes per part: box, input, icon, shortcut, clear, content, scopes, scope, list, group, result, seeAll, recent, recentRemove, clearRecent, status, footer, searchFor, mobileTrigger, overlay, cancel. |

## Examples

- **Static results**: Results filtered locally, without a server.
- **Scopes**: Chips above the results; Tab and Shift+Tab switch them.
- **Phone**: The full-screen search behind an icon button.
- **Phone field**: mobileTrigger=“field” under a large title.
- **API error**: A failed request with Try again.
- **Custom rows**: Avatars, amounts and status badges; see all.
- **Sizes and variants**: sm / md / lg in soft, outline and ghost.

## See also

- [Command Menu](https://wingo-ui.com/components/command-menu) ([Markdown](https://wingo-ui.com/components/command-menu.md))
- [Search Input](https://wingo-ui.com/components/search-input) ([Markdown](https://wingo-ui.com/components/search-input.md))
- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))
- [Topbar](https://wingo-ui.com/components/topbar) ([Markdown](https://wingo-ui.com/components/topbar.md))
- [Kbd](https://wingo-ui.com/components/kbd) ([Markdown](https://wingo-ui.com/components/kbd.md))

---

Canonical: https://wingo-ui.com/components/global-search
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
