DocumentationComponentsTheme Creator
Theme CreatorGitHubIntroduction
InstallationUsageThemingMotion
Components
CheckboxRadioSwitchTextareaNumberFieldOTP InputSliderCircularSliderRatingSelectPickerComboboxSearchSearchSuggestTokenizerSegmentedControl
AI agentsContributing
  1. Components
  2. SearchSuggest

SearchSuggest

A search field with a typeahead suggestions popup on Base UI Autocomplete — recents, grouped suggestions, match highlighting, action rows, async states and a full-screen mobile view.

$ pnpm add @tessinaui/ui

When to use

  • A search field that helps finish the query: recents, popular searches, typeahead, grouped suggestions by entity type.
  • Free-form text stays free-form. The user can always search for exactly what they typed; a suggestion only fills the field and nothing is committed as a value.
  • Recents before anything is typed. Set openOnFocus and let people remove a stale row with onRemove.
  • Next steps beside the results: "See all results" or "Ask AI" as SearchSuggest.Action rows, which never fire onSearch.
  • Remote results with honest loading, error and empty states: pass filter={null} and swap items yourself.
  • A placeholder that says what can be searched ("Search docs…"), not a bare "Search".

When not to use

  • When the field needs no popup. Use Search. Recents shown as page content under the field are a Search composition (search-recent).
  • To pick a value from a list (a country, an assignee). Use Combobox, which commits a selection; SearchSuggest never does.
  • For a ⌘K overlay of commands, navigation and rich result rows. Use Command. Rows with title, description, meta and badges anatomy belong there.
  • For facet tokens inside the field ("in:Products ×"). Use Tokenizer.
  • When the set is small and fits in one view. Show the options instead of asking people to search for them.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { SearchSuggest } from "@tessinaui/ui/search-suggest";
example.tsx
<SearchSuggest.Root items={pages} onSearch={(query, { source }) => runSearch(query)}>
  <SearchSuggest.Input searchLabel="Search docs" placeholder="Search docs…" />
  <SearchSuggest.Popup>
    <SearchSuggest.List>
      <SearchSuggest.Collection>
        {(page: string) => (
          <SearchSuggest.Item key={page} value={page}>
            <SearchSuggest.Highlight text={page} />
          </SearchSuggest.Item>
        )}
      </SearchSuggest.Collection>
      <SearchSuggest.Empty />
    </SearchSuggest.List>
  </SearchSuggest.Popup>
</SearchSuggest.Root>

The field is the house Search: every Search prop (variant, intent, tone, label, clear, submit, shortcut, cancel) works on SearchSuggest.Input. Base UI's Autocomplete input sits inside it, so the text stays free-form — a suggestion only fills the field. Nothing is committed as a value.

items takes a flat array or sections ({ items: [...] }[]). Typing narrows them with a locale-aware "contains" match; function children on List, Collection and Group render what is left. For remote results pass filter={null} and swap items yourself.

onSearch(query, { source }) fires when the user asks for results: "input" for Enter with nothing highlighted or the submit button, "suggestion" when a suggestion is chosen. Action rows never fire it.

Examples

Default

Type to narrow, arrow to highlight, Enter to choose. The field announces the result count through its always-mounted status region.

Grouped suggestions

Sections by entity type, each a role="group" named by its label. Rows take leadingIcon or leadingAvatar and a description line that stays out of the accessible name. positionerProps lets the panel grow wider than the field — still capped by the space available.

Recent searches

openOnFocus shows recents before anything is typed. onRemove makes a row removable — Shift+Delete (or Shift+Backspace) on the highlighted row, or the ×. fillable adds ↖, which puts the suggestion in the field without searching. "Clear recent searches" is an action row with keepOpen.

Match highlighting

match (default) tints what was typed. inverse keeps the typed part muted and gives the completion full ink — the style most shopping apps use for query suggestions. Neither changes weight.

View all and Ask AI

SearchSuggest.Action rows do something with the query — see all results, ask AI. They sit in the arrow-key order, never fire onSearch, never rewrite the field, and are not counted as results. Children can be a function of the query.

Async, loading and error

Remote results: filter={null} and the results as items. Loading marks the list busy, shows the field's spinner and announces "Searching…"; Error announces its label. Put Retry in an action row with keepOpen, so it is keyboard-reachable and the panel stays up. This example fails its first lookup on purpose.

No results

Empty shows only when no suggestion is left, echoes the query, and pairs with a recovery action — the reason and the way forward.

Trending

Before anything is typed, offer what is popular; typing swaps in query suggestions (here with inverse highlighting).

Footer, keyboard hints and shortcut

Footer sits below the listbox, outside it, so it may hold links and buttons. KeyboardHints renders ↑↓ / ↵ / esc and hides wherever touch may be the pointer. showShortcut on the input binds ⌘K / Ctrl+K.

Mobile: full-screen view

Below md the default mobilePresentation="fullscreen" turns the open field into a full-screen search view — field and Cancel at the top, the list filling the rest, the page behind locked. "dropdown" keeps the anchored panel. Narrow the window below 768px to compare.

Locations

A static "Current location" row before the collection, places with a context line ("Airport in Paris") and a trailing kind. itemToStringValue names object items.

Attached panel

attached joins the panel to the field while open. Use it with rounded none–lg; a pill has no straight edge to share.

On-color

Inside a coloured Surface the field inherits tone="on-color": the well, ink, placeholder and focus ring take the plate's ink. The suggestion panel and the phone's full-screen view are page surfaces, so their rows and field keep the page's inks whatever plate the field sits on.

RTL

dir on the root mirrors the field and is restated on the portalled panel, so rows put their icon on the right and trailing text on the left, and the fill arrow flips to point back at the field. Leave dir unset and nothing is stamped: the field and panel follow the page.

API Reference

SearchSuggest.Root

PropTypeDefaultDescription
itemsT[] | { items: T[] }[]—Suggestions, flat or in sections. Filtered by the query unless filter={null}
filteredItemssame—Already-filtered suggestions; skips filtering and limit
filter(item, query, itemToString) => boolean | nullcontainsCustom match, or null for none
limitnumber—Most suggestions rendered across all sections
itemToStringValue(item: T) => stringlabel/value/StringText for object items — what a chosen suggestion writes into the field
value / defaultValue / onValueChangestring / string / (value, { reason }) => void— / "" / —The query text. reason is Base UI's (input-change, item-press, …) or fill
open / defaultOpen / onOpenChangeboolean / boolean / (open, { reason }) => void— / false / —Popup state
openOnFocusbooleanfalseOpen on focus with nothing typed; clearing keeps it open
onSearch(query, { source: "input" | "suggestion" }) => void—The user asked for results
mode"list" | "both" | "inline" | "none""list"Base UI completion: both also inline-completes the highlighted suggestion
autoHighlight / keepHighlight / highlightItemOnHover / loopFocus—Base UI defaultsHighlight behaviour
statusTextstringautomaticOverride the announcement ("Searching…", "N results", "No results for “q”")
size / roundedSearch scale"md" / "full"Shared by the field and the panel
mobilePresentation"fullscreen" | "dropdown""fullscreen"Below md
attachedbooleanfalseJoin the panel to the field
disabled / readOnly / required / name / form——Field state and form participation
dir"ltr" | "rtl"—Direction

Other attributes and className land on the root <div>.

SearchSuggest.Input

Every Search prop except value, defaultValue, onValueChange, onSearch, size, rounded, disabled, statusText and renderInput, which the Root owns. loading defaults to "a Loading part is mounted"; showCancelButton defaults to "the full-screen view is showing". The ref is the input.

SearchSuggest.Popup / List

Popup takes positionerProps (side, align, offsets, style for width) and portalProps. List is the listbox and the scroller; its function children map the filtered items. Default aria-label "Suggestions".

SearchSuggest.Group / GroupLabel / Collection / Separator

Group items hands a section's filtered items to a Collection inside it. Collection renders one element per item — of its Group, else of the Root. GroupLabel names the group and cannot hold a control.

SearchSuggest.Item

PropTypeDefaultDescription
valueT—The suggestion
leadingIcon / leadingAvatarReactNode—Leading visual
descriptionReactNode—Second line; the option's description, not its name
trailingTextReactNode—Trailing muted meta; outside the name
onRemove / removeLabel() => void / string— / "Remove"Removable row: Shift+Delete / Shift+Backspace, or the pointer ×
fillable / fillLabelboolean / stringfalse / "Fill search"Pointer ↖ writes the suggestion into the field without searching

SearchSuggest.Action

PropTypeDefaultDescription
childrenReactNode | (query) => ReactNode—Row label
onAction(query: string) => void—Click or Enter
keepOpenbooleanfalseLeave the popup open
leadingIcon / trailingTextReactNode—Row decoration

SearchSuggest.Highlight

text · query (defaults to the current query) · mode="match" | "inverse".

SearchSuggest.Empty / Loading / Error

Empty — default copy "No results for “q”" / "No suggestions". Loading — label ("Searching…"), variant="spinner" | "skeleton", rows. Error — label ("Couldn’t load suggestions").

SearchSuggest.Footer / KeyboardHints

Footer is a plain row below the list. KeyboardHints takes navigateLabel, selectLabel, closeLabel.

Keyboard Interactions

KeyAction
TypingNarrows suggestions; opens the popup
↓ / ↑Opens the popup / moves the highlight; focus stays in the field
Alt+↓Opens the popup without moving the highlight
EnterChooses the highlighted row; with nothing highlighted, searches the typed query
EscapeCloses the popup and keeps the query; with the popup closed, clears the field
Shift+Delete / Shift+BackspaceRemoves the highlighted row when it has onRemove
Home / EndMove the caret in the field
⌘K / Ctrl+KFocuses the field (showShortcut)
TabLeaves the field; the popup closes

Accessibility

VoiceOver + Safari: passed 2026-09-14. Focus reads the placeholder and the hint, "Type, arrow down, press Enter."; typing announces "20 suggestions"; Down reads "Accordion (1 of 20)"; Escape reads "collapsed". NVDA and JAWS are untested.

  • The input is a role="combobox" inside the Search landmark, with aria-expanded always present, aria-autocomplete="list", aria-activedescendant for the highlight and autocomplete="off". aria-controls points at the listbox only while it is open.
  • Result counts are announced from the field, not the popup: Search's status region is always in the DOM and empty until there is something to say. A region inside the popup would arrive already full when it opens, and a region arriving full is not announced.
  • The listbox owns only options and groups. Empty and Error are disabled options; Loading marks the list aria-busy. Remove and fill are pointer affordances with keyboard equivalents, never buttons inside an option.
  • While the popup is open Base UI hides everything outside the field input and the popup from assistive tech — Search's clear, submit and Cancel buttons included. Screen-reader users close it with Escape or the "Dismiss" control Base UI places before the field and after the list.
  • Rows keep the 44px touch floor; the × and ↖ affordances carry their own 44px hit areas on touch.
  • Highlighting marks with a tint or ink, never weight, and the option's name is its unbroken label text.
  • Known limits: iOS VoiceOver and TalkBack ignore aria-activedescendant — every row still works by tap. The pointer × has no swipe equivalent; offer "Clear recent searches" as the reachable path.

Agent notes (AI-first)

data-slot: search-suggest-root · search-suggest-popup · search-suggest-list · search-suggest-group · search-suggest-group-label · search-suggest-separator · search-suggest-item · search-suggest-item-trailing · search-suggest-item-fill · search-suggest-item-remove · search-suggest-action · search-suggest-highlight · search-suggest-empty · search-suggest-loading · search-suggest-error · search-suggest-footer · search-suggest-keyboard-hints, plus every Search slot on the field. The root stamps data-component="search-suggest", data-open and data-takeover.

States an agent can read: [data-slot="search-status"] text (count, loading, error), aria-expanded on the input, aria-busy on the list.

example.tsx
// Copy-paste: remote suggestions with loading, error and a see-all row
<SearchSuggest.Root items={results} filter={null} value={query} onValueChange={setQuery} onSearch={runSearch}>
  <SearchSuggest.Input searchLabel="Search orders" placeholder="Search orders…" />
  <SearchSuggest.Popup>
    <SearchSuggest.List>
      {isPending && <SearchSuggest.Loading />}
      {error && <SearchSuggest.Error />}
      <SearchSuggest.Collection>
        {(order: Order) => (
          <SearchSuggest.Item key={order.id} value={order} description={order.customer}>
            <SearchSuggest.Highlight text={order.number} />
          </SearchSuggest.Item>
        )}
      </SearchSuggest.Collection>
      <SearchSuggest.Empty />
      <SearchSuggest.Action onAction={openAllResults}>{(q) => `See all results for “${q}”`}</SearchSuggest.Action>
    </SearchSuggest.List>
  </SearchSuggest.Popup>
</SearchSuggest.Root>

Do: keep rows to one line plus an optional description · put actions in SearchSuggest.Action · give object items itemToStringValue when they have no label. Don't: put buttons or links inside an item · use it to pick a form value (that is Combobox) · build result cards in the popup (that is Command).

SearchTokenizer

On this page

When to useWhen not to useInstallationUsageExamplesDefaultGrouped suggestionsRecent searchesMatch highlightingView all and Ask AIAsync, loading and errorNo resultsTrendingFooter, keyboard hints and shortcutMobile: full-screen viewLocationsAttached panelOn-colorRTLAPI ReferenceSearchSuggest.RootSearchSuggest.InputSearchSuggest.Popup / ListSearchSuggest.Group / GroupLabel / Collection / SeparatorSearchSuggest.ItemSearchSuggest.ActionSearchSuggest.HighlightSearchSuggest.Empty / Loading / ErrorSearchSuggest.Footer / KeyboardHintsKeyboard InteractionsAccessibilityAgent notes (AI-first)