DocumentationComponentsTheme CreatorGitHub
Theme CreatorGitHubIntroduction
InstallationUsageTheming
ComponentsAccordionAction SheetAlertAlertDialogArea ChartAspectRatioAvatarBadgeBannerBar ChartBottom NavBreadcrumbButtonButtonGroupCalendarCardCarouselChartChatBubbleChatBubbleNewCheckboxChipCoachMarkCodeBlockCollapsibleColor PickerComboboxCommandContainerContextMenuDate PickerDividerDrawerDropdown MenuEmptyStateFABFieldFieldsetFile UploadFlexFormGridHoverCardIconButtonLabelLine ChartLinkMenubarMeterModalNavigation MenuNumberFieldOTP InputPaginationPickerPie ChartPopoverProgressPromptInputRadar ChartRadial ChartRadioRatingScroll AreaSearchSegmentedControlSelectShortcutSidebarSkeletonSliderCircularSliderMediaTrimmerSpacerSpinnerSplit ButtonStackStatusStepperSurfaceSwitchTableTabsTextareaTime PickerToastToggleButtonToggleGroupTokenizerToolbarTooltipTop Header DesktopTop Header Mobile
Contributing
Components

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.

Playground

Installation

pnpm add @tessinaui/ui

Usage

import { Search } from "@tessinaui/ui";
<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 tracked as SearchSuggest (TES-158) on Base UI Autocomplete.

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 phones so iOS Safari never zooms the page on focus.

Intents

intent + supportingText for feedback. A note on honesty: no surveyed design system documents validation states on a search field — this axis exists for house parity with Field. Use it for genuine feedback ("No results", a syntax error in a query language), not decoration.

On-color

tone="on-color" renders the field on a coloured or dark plate — hero bands, brand headers (six research apps put a white-on-brand search in the header). 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.

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

Budget: at most two actions on phones (M3's cap). Each control grows a 44px tap extension on touch viewports, so the run spaces itself at 12px below md and every action keeps a full 43×43 exclusive hit area; 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 HIG 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

The iOS pattern: 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. (A suggestions popup is TES-158.)

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

Carbon 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 GitHub/Linear 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

API Reference

Search

PropTypeDefaultDescription
variant"default" | "filled" | "minimal" | "ghost""default"Visual variant. ghost is chromeless for toolbars and keeps the full focus ring
size"xs" | "sm" | "md" | "lg" | "xl""md"Heights 32/36/40/48/56; xs–md grow to the 44px floor on phones
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
searchLabelstring"Search"Accessible name for the landmark (and the input when no label)
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" / "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 <output> 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" / —External iOS-style cancel, visible while focused or dirty
expandable / expanded / defaultExpanded / onExpandedChange—false / — / false / —Icon-only collapsed mode with Carbon expand/collapse mechanics
noFormbooleanfalse<div role="search"> wrapper for use inside another form
dir"ltr" | "rtl"—Direction; layout is logical-property driven
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…"Placeholder-style text; part of the accessible name
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 (§2b parity).

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 → clear (when visible) → trailing actions → submit → cancel

Accessibility

  • <form role="search"> landmark (or <div role="search"> under noForm); give each landmark a unique searchLabel when a page has several.
  • With no visible label, searchLabel names both the landmark and the input — screen readers will read it on both; pass a visible or hidden label when that double-speak matters in your layout.
  • 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 the GitHub-pattern 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 phones; 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.

// 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 Combobox/TES-158) · render two bound showShortcut fields expecting both to fire · rely on placeholder as the only label.

Scroll AreaSegmentedControl

On this page

PlaygroundInstallationUsageExamplesDefaultVariantsSizesIntentsOn-colorLeading iconTrailing actionsScopeFilter chipsWith submit buttonKeyboard shortcutCancel buttonStatus and loadingRecent searchesNo resultsLabelledInside another formExpandableTrigger for a command paletteSkeletonAPI ReferenceSearchSearchTriggerSearchSkeletonKeyboard InteractionsAccessibilityAgent notes (AI-first)