Skip to main content

SearchField

stablev0.2.0

Specialized text input for search and filtering. Wraps a native <input type="search"> with a leading search icon, a built-in clear cross (an IconButton, shown only while the field holds a value), an optional trailing action slot (searchOption), an optional trailing segmented-control slot (buttonGroup) that sits flush against the trailing edge, and Enter-to-submit behavior. Use for data table filtering, list search, and platform-wide search bars.

Variants

Examples

<SearchField aria-label="Search items" onSubmit={(value) => console.log(value)} placeholder="Search…" />
<SearchField aria-label="Filter results" size="sm" onChange={(e) => console.log(e.target.value)} />
<SearchField aria-label="Search items" value={keyword} onChange={(e) => setKeyword(e.target.value)} onClear={() => setKeyword("")} />
<SearchField aria-label="Search items" clearLabel="Clear filter" onClear={() => refetch({ search: "" })} searchOption={<IconButton icon={<Icon name="settings" size={16} />} aria-label="Search options" onClick={openOptions} variant="default" priority="tertiary" size="sm" />} />
// the header composition (Figma assembly 6958:19681): a segmented control flush at the field's right edge
<SearchField
  aria-label="Search"
  fullWidth
  buttonGroup={
    <ButtonGroup corner="end" value={mode} onValueChange={setMode} aria-label="Search mode">
      <ButtonGroupItem value="filters" aria-label="Filters" icon={<Icon name="settings" size={16} />} />
      <ButtonGroupItem value="advanced" aria-label="Advanced search" icon={<Icon name="search" size={16} />} />
    </ButtonGroup>
  }
/>
<SearchField aria-label="Search unavailable" disabled placeholder="Search unavailable" />

Props

PropType
valuestring — Controlled value
defaultValuestring — Default value (uncontrolled)
onChange(event: ChangeEvent<HTMLInputElement>) => void — Fires on each keystroke
onSubmit(value: string) => void — Fires when user presses Enter
onClear() => void — Fires when the built-in clear cross is clicked or Escape is pressed. Both paths are identical and always fire it: uncontrolled, the component also empties the input itself; controlled, the consumer resets `value` in response. `onChange` is NOT re-fired on clear — this is the signal to listen to. REQUIRED to get a clear cross in controlled mode: the component never overwrites a `value` it does not own, so without `onClear` the cross could do nothing and is not rendered at all (SearchField.rfc.md §9 D4).
clearLabelstring — Accessible label for the built-in clear cross (default: "Clear search")
searchOptionReactNode — Trailing action slot (Figma: searchOption), rendered after the built-in clear cross. Accepts IconButton(s) for filter, options, etc. Never put a clear button here — the component already renders one (SearchField.rfc.md RULE-08).
buttonGroupReactNode — Trailing segmented-control slot (Figma: buttonGroup, node 2682:8640 / instance 7333:42499), rendered last. Pass a <ButtonGroup corner="end"> so its trailing corners follow the field's radius while its leading edge stays flush against the input. Filling this slot removes the field's right padding: in the reference header assembly (6958:19681) the group spans x 292→400 inside a 400-wide field, so its right edge IS the field's. COMPOSITION only — SearchField restates none of ButtonGroup's styles.
size"sm" | "md" — Size variant (default: "md")
fullWidthboolean — Expand to fill container width
placeholderstring — Placeholder text (default: "Search…")
disabledboolean — Disabled state
classNamestring — Additional CSS classes

Accessibility

2.1 AAWCAG pass

Contrast: the disabled text on the disabled field fill (--text-input-disabled on --bg-input-disabled) is permanently exempt — WCAG 1.4.3 Note 1 excludes inactive user interface components — so no contrast pair is declared for it, deliberately and not by omission. Measured 4.35:1 dark / 3.69:1 light since the 2026-08-20 export moved both tokens onto the gray ramp. Same non-declaration as Menu's disabled row, which renders the identical two classes. Native <input type="search"> wrapped in role="search" landmark. Search icon is decorative (aria-hidden). The built-in clear cross is an IconButton with its own aria-label (clearLabel), rendered only while the field holds a value AND clicking it can actually clear something — never as an inert control, so a controlled field with no onClear gets no cross rather than a button whose accessible name promises an action it cannot perform (SearchField.rfc.md §9 D4); its glyph is decorative and it is disabled when the field is. Every browser-painted clear widget is suppressed (::-webkit-search-cancel-button, ::-webkit-search-decoration, ::-ms-clear) so exactly one cross exists in the field and clearing always fires onClear — the OpenAEV pilot found the native cross was emptying the field silently (SearchField.rfc.md §9). Enter submits; Escape and the clear cross share one clearing path. Both are defaults a consumer can decline: `onKeyDown` runs first and `preventDefault()` vetoes the built-in behaviour, so a combobox wrapper can own Escape without the field being emptied under it (SearchField.rfc.md §9 D5). Focus indication uses ring-focus utility (2px ring, brand-primary) per Figma spec and AGENTS.md §6.2 — consistent with other components. searchOption slot icons must carry their own aria-label. searchOption is inert when disabled (no keyboard access). buttonGroup is inert when disabled too, on the same mechanism; the group keeps its own radiogroup semantics and roving focus — SearchField adds no ARIA of its own around it, so the field stays one search landmark containing one radiogroup rather than nesting roles.