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

Search

A search field with four variants, tone-aware on-color rendering, extensible leading/trailing slots, submit and cancel affordances, an expandable icon-only mode, a button-shaped trigger for command palettes, and a WCAG 4.1.3 status region.

$ pnpm add @tessinaui/ui

When to use

  • A large or complex set of content that people cannot scan in one view: a catalogue, an inbox, a docs site, a table.
  • At any scope. Site-wide in a header, page-level for one page, or inside a component such as a data table. Name the scope in the placeholder or with a scope tab.
  • A field that submits a query. Enter or the inline submit calls onSearch, and a permanently mounted status region announces loading and result counts.
  • A compact toolbar slot. expandable collapses the field to an icon until it is needed, and variant="ghost" keeps it quiet.
  • A button that opens a search surface. SearchTrigger looks like the field and opens a command palette (Trigger).

When not to use

  • For a small set that fits on screen. If everything is visible and easy to find, a search field is only more to learn.
  • For suggestions in a popup under the field. Use SearchSuggest, which is built on this field.
  • To pick one option from a list. Use Combobox.
  • For commands and navigation in a palette. Use Command.
  • For an AI or chat composer. Use PromptInput.
  • For facet tokens inside the input. Use Tokenizer.

Installation

Terminal
pnpm add @tessinaui/ui

Usage

example.tsx
import { Search } from "@tessinaui/ui";
example.tsx
<Search placeholder="Search..." onSearch={(value) => console.log(value)} />

The component wraps a native <input type="search"> in a <form role="search"> landmark. It ships the mobile keyboard contract by default (enterKeyHint="search", inputMode="search", autoComplete="off", auto-capitalise/correct off), handles Escape to clear and Enter to submit, and announces loading and result counts through a permanently mounted polite status region.

Boundaries. Search is the field shell. A filtered listbox is Combobox; a command palette is Command; a chat/AI composer is PromptInput; facet tokens inside the input are Tokenizer. A search field with a suggestions popup is SearchSuggest, built on this field.

Examples

Default

Variants

Four visual styles — default (bordered), filled (background), minimal (bottom border only) and ghost (chromeless, for toolbars). Ghost keeps the full focus ring: a chromeless rest state never licenses a chromeless focus state.

Sizes

Five sizes on the input-family height ladder (32/36/40/48/56). Input text follows the family type ladder — 12/14/14/16/16 on desktop, floored at 16px on touch devices so iOS Safari never zooms the page on focus.

Intents

intent + supportingText for feedback. This axis exists for parity with Field. Use it for genuine feedback ("No results", a syntax error in a query language), not decoration.

Error message

errorMessage replaces supportingText while present, implies intent="error", joins the input's aria-describedby and is announced politely.

On-color

tone="on-color" renders the field on a coloured or dark plate — hero bands, brand headers. Inherited automatically inside a Surface.

Leading icon

Replace the magnifier with a back chevron (pushed search screens), a semantic icon, or null for the icon-less field.

Trailing actions

Extra controls — voice, camera, a filter toggle, a scope select — live in trailingActions, with SearchActionsDivider marking the text-actions/modality break. Run order in the field is fixed: shortcut cap → actions → clear → submit. The clear button always stays adjacent to the text it clears.

Spread searchActionProps(size, rounded) onto every control in the slot.

The slot takes any node, so nothing stops you sizing an action by hand — and then it will not match the field's own clear chip. The helper hands each control the clear chip's box, radius and tap-extension edge at every search size (32px at md, 40px at lg, …), which is what makes an action and the clear × read as one family. Mirrors Field's fieldSuffixButtonProps.

example.tsx
<Search
  trailingActions={
    <IconButton {...searchActionProps("md")} variant="ghost" icon={<Mic />} aria-label="Voice search" />
  }
/>

Budget: at most two actions on phones. On touch devices each control grows a 44px tap extension toward the field's start, so the field spaces every control, the clear chip included, by that extension's overhang: 20px beside the 24px chips of xs and sm, 12px beside the 32px chips of md. Every action keeps a full 44×44 target; a third does not fit a compact field at 320px. If you need more, move them behind a menu or below the field.

Scope

Scope tabs under the field (the scope-bar shape) plus a scoped placeholder naming the corpus.

Filter chips

An attached chip row narrows the result set; the group owns the selection.

With submit button

showSubmitButton renders the inline submit; submitVariant="icon" is the square icon form (the more common shape in the wild). The icon submit takes its accessible name from submitButtonText.

Keyboard shortcut

showShortcut renders the ⌘K cap (Esc once dirty) and binds a global Cmd/Ctrl+K focus shortcut. The binding is separable (shortcutBinding={false}), the LAST mounted bound instance wins, and the listener never fires while another editable control has focus.

Cancel button

A text button outside the field, visible while focused or dirty. It clears, blurs and calls onCancel — dismissal, not just clearing.

Status and loading

The WCAG 4.1.3 contract: statusText feeds an always-mounted polite live region ("Searching…", "12 results", "No results for …"); showStatus mirrors it visibly. loading swaps the icon for a spinner AND announces — a silent spinner is invisible to assistive tech.

Recent searches

Clock-keyed rows, per-row remove, section-level Clear all — as page content under the field. For recents in a popup on focus, use SearchSuggest.

No results

Echo the query in quotes, offer a next step, mirror the zero count through statusText. When a filter caused the emptiness, say so and offer to remove the filter instead — that is a different state than a bad query.

Labelled

The Field-parity label row: label, required/optional, right-aligned infoText, visuallyHiddenLabel.

Inside another form

Nested forms are invalid HTML. noForm renders a <div role="search"> landmark instead — Enter still submits through the keydown path and the inline submit becomes type="button".

Expandable

The mechanics: collapsed to an icon button; expanding focuses the input; focus leaving an empty field collapses it; Escape on an empty field collapses and returns focus to the trigger.

Trigger for a command palette

SearchTrigger is a button dressed as the field — the surveyed pattern for opening a search surface. Pair it with Command.Dialog and useCommandShortcut; the trigger carries aria-haspopup="dialog" and folds the shortcut into its accessible name.

Skeleton

RTL

dir="rtl" moves the magnifier to the right edge and the trailing slot (clear, shortcut cap or submit button) to the left. The label and helper rows align right. Leave dir unset and the field follows the page's direction.

API Reference

Search

PropTypeDefaultDescription
variant"default" | "filled" | "minimal" | "ghost" | "underline""default"Visual variant. ghost is chromeless for toolbars and keeps the full focus ring. underline is an alias of minimal, the input family's name for the bottom-rule shell
size"xs" | "sm" | "md" | "lg" | "xl""md"Heights 32/36/40/48/56; xs–md grow to the 44px floor on touch devices
rounded"full" | "lg" | "md" | "sm" | "none""full"Corner treatment. Ignored by minimal
intent"none" | "error" | "warning" | "success" | "info""none"Border/ring/supporting-text semantics
tone"default" | "on-color"inheritedOn-color rendering for coloured plates; inherited from Surface
label / visuallyHiddenLabel / required / optional / infoText——Field-parity label row
supportingTextstring—Text below the field; colour and icon follow intent. Wired to the input's aria-describedby
errorMessageReactNode—Error row below the field. Replaces supportingText, implies intent="error", sets aria-invalid and is announced politely
searchLabelstring"Search", Arabic under dir="rtl"Accessible name for the input when there is no label; with a label, it names the landmark instead
clearable / onClear—true / —Clear button (tab-reachable). Clearing dispatches a real change event, then refocuses the input
onSearch(value: string) => void—Enter or submit button
onValueChange(value: string) => void—Canonical string change callback (native onChange also forwarded)
showSubmitButton / submitButtonText / submitVariant—false / "Search" (Arabic under dir="rtl") / "label"Inline submit; "icon" renders the square icon form named by submitButtonText
loadingbooleanfalseSpinner in the leading slot + aria-busy + polite "Searching…" announcement
statusText / showStatusstring / boolean— / falseFeeds the always-mounted polite live region; showStatus renders it visibly too
leadingIconReactNode | nullmagnifierReplace or (null) remove the leading slot
trailingActionsReactNode—Extra controls between the shortcut cap and the clear button
showShortcut / shortcutBindingbooleanfalse / true⌘K cap; binding separable, last-mounted-wins, typing-aware
showCancelButton / cancelLabel / onCancel—false / "Cancel" (Arabic under dir="rtl") / —External text Cancel button, visible while focused or dirty
expandable / expanded / defaultExpanded / onExpandedChange—false / — / false / —Icon-only collapsed mode with the expand/collapse mechanics above
noFormbooleanfalse<div role="search"> wrapper for use inside another form
dir"ltr" | "rtl"—Direction; layout is logical-property driven
stringsPartial<SearchStrings>English, Arabic under dir="rtl"Override the clear button's name (clear), the optional marker, the defaults of searchLabel, submitButtonText, cancelLabel and placeholder ("Search..."), and the searching announcement. The props win
wrapperClassName / containerClassName / classNamestring—Wrapper / container / input class hooks

Native input attributes are forwarded. The mobile defaults (enterKeyHint="search", inputMode="search", autoComplete="off", autoCapitalize="none", autoCorrect="off", spellCheck={false}) are plain props — pass your own to override.

SearchTrigger

PropTypeDefaultDescription
labelstring"Search…", Arabic under dir="rtl"Placeholder-style text; part of the accessible name
stringsPartial<Pick<SearchStrings, "triggerLabel">>English, Arabic under dir="rtl"Override the default label (triggerLabel)
showShortcutbooleantrueRenders the ⌘K cap and appends "(⌘K)" to the accessible name
variant / size / rounded / intent / tone——Same shell axes as Search

Plus all native button props. The trigger never binds the shortcut itself — that belongs to the surface it opens (useCommandShortcut).

SearchSkeleton

size · rounded · variant · showLabel · showHelper — same box as the real field at every combination.

Keyboard Interactions

KeyAction
EnterSubmits (calls onSearch); consumer onKeyDown runs first and may preventDefault()
EscapeClears a dirty field; collapses an empty expandable field back to its trigger
⌘K / Ctrl+KFocuses the field (showShortcut, unless shortcutBinding={false}); never fires while typing in another editable control
TabField → trailing actions → clear (when visible) → submit → cancel

Accessibility

  • <form role="search"> landmark (or <div role="search"> under noForm). This is the only search landmark when composed into a header — the TopHeaderMobile and TopHeaderDesktop search slots are plain boxes and add none.
  • searchLabel names exactly one element, so screen readers say it once. With no label it names the input and the landmark stays unnamed, which is fine for the usual single search on a page. With a label (visible or visuallyHiddenLabel) the label names the input and searchLabel names the landmark. When a page has several searches, give each a label and a unique searchLabel so the landmarks can be told apart.
  • The status region satisfies WCAG 4.1.3: it is ALWAYS in the DOM (a region inserted at announce time is unreliable), polite, and fed by statusText/loading. Announce counts ("12 results", "No results"), not the result list itself.
  • The clear button is a real tab stop that returns focus to the input and dispatches a native change event — form libraries observe clears.
  • The ⌘K cap is decorative (aria-hidden); the binding is modifier-based, so WCAG 2.1.4 does not apply. Never make a bare-key shortcut the only path.
  • SearchTrigger follows that pattern's contract: a button (aria-haspopup="dialog"), shortcut folded into the accessible name (2.5.3 Label in Name), cap decorative.
  • Touch targets: the container floors at 44px on touch devices; the inner clear chip carries its own tap extension.

Agent notes (AI-first)

Every part is addressable: data-slot = search-root · search-container · search-input · search-icon · search-clear · search-shortcut · search-actions · search-actions-divider · search-submit · search-cancel · search-label · search-supporting · search-status · search-trigger · search-expand-trigger · search-skeleton. The root also stamps data-component="search", data-variant, data-size, data-intent, data-tone.

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

example.tsx
// Copy-paste: instrumented search with status + submit
<Search
  searchLabel="Search orders"
  onValueChange={setQuery}
  onSearch={runSearch}
  loading={isPending}
  statusText={isPending ? "Searching…" : `${results.length} results`}
  showSubmitButton
  submitVariant="icon"
/>

Do: feed statusText whenever results change · use noForm inside forms · give concrete example-query placeholders ("Try “wireless headphones”"). Don't: bolt a listbox onto this component (that is SearchSuggest or Combobox) · render two bound showShortcut fields expecting both to fire · rely on placeholder as the only label.

ComboboxSearchSuggest

On this page

When to useWhen not to useInstallationUsageExamplesDefaultVariantsSizesIntentsError messageOn-colorLeading iconTrailing actionsSpread searchActionProps(size, rounded) onto every control in the slot.ScopeFilter chipsWith submit buttonKeyboard shortcutCancel buttonStatus and loadingRecent searchesNo resultsLabelledInside another formExpandableTrigger for a command paletteSkeletonRTLAPI ReferenceSearchSearchTriggerSearchSkeletonKeyboard InteractionsAccessibilityAgent notes (AI-first)