Search Input
SearchInput is an Input with type="search": a leading
icon, a clear button while the field has a value, a spinner while
loading, an onSearch callback on Enter and Escape-to-clear. Every
Input prop applies except type and leadingContent. A root <div>
wraps the Input, so className and fullWidth go on that root and
inputClassName onto the native input.
import SearchInput from '@zuilib/components/search-input'
Basic
Type to reveal the clear button; Escape or the button empties the field
through a native input event, so onChange fires with '' and a
controlled value follows without extra wiring.
Loading
loading adds a trailing spinner, sets aria-busy and announces
loadingLabel from a visually hidden role="status" region. Enter is
ignored while loading.
Sizes and icon
sm, md and lg come from the Input's control tokens; the icons step
with them. icon={false} removes the leading glyph; any node replaces it.
In a FormItem
The inner Input receives the item's id, aria-labelledby and
aria-describedby; disabled is inherited when left unset (and hides the
clear button).
Props
SearchInputProps extends InputProps (variant, state, invalid,
unsaved, fullWidth, inputClassName, trailingContent, native input
attributes) minus type and leadingContent, with:
| Prop | Type | Default | Description |
|---|---|---|---|
onSearch | (value: string) => void | — | Plain Enter (no modifier, not during IME composition, not while loading). When set, Enter is consumed and does not submit an enclosing <form> |
onClear | () => void | — | After the clear button or Escape empties the field; onChange fires with '' first |
loading | boolean | false | Trailing spinner, aria-busy, and loadingLabel in a visually hidden role="status" region |
loadingLabel | string | 'Loading' | Announced while loading |
clearLabel | string | 'Clear' | Accessible name of the clear button |
icon | ReactNode | false | magnifying glass | Leading glyph, aria-hidden; false renders none |
trailingContent | ReactNode | — | After the spinner and the clear button |
size | 'sm' | 'md' | 'lg' | 'md' | |
fullWidth | boolean | false | w-full on the root |
className | string | — | Merged onto the root <div> |
inputClassName | string | — | Merged onto the native <input> |
The ref points at the native <input>. An imperative
ref.current.value = … fires no event; dispatch an input event after it
(or control the field) for the clear button to notice.
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="search-input"] | div | Root. Also data-size, data-loading, data-has-value |
[data-slot="search-input-control"] | div | The Input frame (its data-slot="input" is replaced). Same data attributes as the root |
[data-slot="input-control"] | input | The native type="search" input, browser cancel glyphs removed |
[data-slot="search-input-icon"] | span | Leading glyph; aria-hidden |
[data-slot="search-input-spinner"] | svg | Present while loading |
[data-slot="search-input-status"] | span | Visually hidden role="status" with loadingLabel |
[data-slot="search-input-clear"] | button | Present while there is a value and the field is enabled and editable |
[data-slot="search-input-clear-icon"] | svg | — |
/* Consumer CSS: keep the clear button visible on hover only */
[data-slot="search-input"]:not(:hover):not(:focus-within) [data-slot="search-input-clear"] {
opacity: 0;
}
Tokens
| Token | Used for |
|---|---|
--control-height-*, --control-padding-x-* | Field box per size (from Input) |
--input, --background, --ring | Frame border, fill and focus ring |
--muted-foreground, --foreground | Icon, spinner and clear button colours |
--radius-sm | Clear button corners |
--duration-fast | Clear button colour transition |
Accessibility
- A real
<input type="search" enterKeyHint="search">; inside aFormItemit gets the field'sid,aria-labelledbyandaria-describedby. Outside one, passaria-labelor aLabel htmlFor. - The leading icon is decorative (
aria-hidden); the clear button is a real button named byclearLabel, and it keeps focus in the input when clicked. - Keyboard: Enter calls
onSearch(plain Enter only; modifier keys and IME composition are left alone); Escape clears a non-empty field and callsonClear. An empty field leaves Escape alone so an enclosing dialog can close. loadingsetsaria-busyon the input and announcesloadingLabelfrom a politerole="status"region.- The clear button hides itself when the field is disabled (also through a Headless UI
Fieldset disabled) or read-only.
Related
- Input: the field this builds on, with the shared variants and states.
- Combobox: search with suggestions.
- NumberInput: another Input with built-in behaviour.
- FormItem: the field wrapper that wires label, description and message.