Source: https://monarchui.dev/components/search-input/

Components · Search and discovery

# SearchInput

Search box with icon, clear button, debounce, pending state and shortcut hint.

@rookcom/monarch-react

Component

## Debounced, pending

## Usage examples

### Shortcut, sizes and Field

## Purpose

A single-line search box for filtering a list or querying a server. It adds the parts every search field re-implements: a leading icon, a clear button, a debounced search callback, a pending state while results load and an optional keyboard shortcut.

### Use when

- Filtering a table, list or grid as the user types.
- Querying a server, where `debounce` keeps requests to one per pause.
- A page-level search reached with a shortcut such as "/" or ⌘K.

### Avoid when

- The user picks one value from a list: use Combobox.
- Free text that is not a query (a name, a title): use Input.
- A command palette with grouped actions and results: use a command menu.

## Design principles

- One callback per concern: `onValueChange` for every keystroke, `onSearch` for when to run the query.
- Clearing is always one step: the clear button or Escape, and both return focus to the field.
- Pending replaces the magnifier with a spinner in the same place, so the layout never shifts.
- The shortcut hint is decorative; `shortcutKeys` is what binds the key and announces it.

## API reference

```tsx
import { SearchInput } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `SearchInput` | component | client | `.mn-search-input` |

Renders a `.mn-search-input` wrapper (styled as `.mn-input`) around a native `<input type="search">`. The ref and all native input props go to the input; `className` goes to the wrapper.

Reads Field context through `useFieldControl`, like Input.

Generated from the TypeScript declarations of `packages/react/src/components/search/search-input`.

## Properties

`onChange` still receives the native change event; `onValueChange` receives the string.

### `SearchInput`

A search box: leading icon, clear button, optional debounce for fetching, a pending state, and an optional keyboard shortcut that focuses it. Renders a native `type="search"` input, so give it a visible label (Field) or an `aria-label`.

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` | Class for the outer wrapper (the input itself is `.mn-search-input__input`). |
| `clearable` | `boolean` | Show a clear button while there is text. Default true. |
| `clearLabel` | `string` | Accessible name of the clear button. Default "Clear search". |
| `debounce` | `number` | Milliseconds to wait after the last keystroke before calling `onSearch`. Default 0 (no wait). |
| `defaultValue` | `string` | Initial query text when uncontrolled. |
| `disabled` | `boolean` |  |
| `icon` | `ReactNode` | Replaces the leading magnifier icon. Decorative. Pass `null` for none. |
| `id` | `string` |  |
| `invalid` | `boolean` | Marks the value invalid. Inside a Field, the Field's `invalid` is used. |
| `onClear` | `(() => void)` | Called after the field is cleared by the button or Escape. |
| `onSearch` | `((query: string) => void)` | Called with the query to run: after `debounce` ms of no typing, at once on Enter, and with `''` when the field is cleared. Use this for fetching. |
| `onValueChange` | `((value: string) => void)` | Called on every keystroke and on clear, with the new text. |
| `pending` | `boolean` | A search is in flight: the leading icon becomes a spinner and the input is `aria-busy`. |
| `shortcut` | `ReactNode` | Hint shown at the trailing edge while the field is empty, e.g. "/" or "⌘K". Decorative. |
| `shortcutKeys` | `string` | The key combination that focuses the field from anywhere on the page, in `aria-keyshortcuts` syntax ("/", "Meta+K", "Control+K"). Setting it binds the listener and sets `aria-keyshortcuts`; single keys are ignored while typing in another field. |
| `size` | `"sm" \| "md" \| "lg"` |  |
| `value` | `string` | The query text (controlled). |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `size` | `'sm'``'md'``'lg'` | Control height from `--mn-control-*`, as for Input. Default `md`. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Pending | `data-pending on the wrapper, aria-busy="true" on the input` | The leading icon becomes a spinner while a search is in flight. |
| Has text | – | The clear button appears; the shortcut hint hides. |
| Invalid | `data-invalid on the wrapper, aria-invalid="true" on the input` | Danger border, from `invalid` or the surrounding Field. |
| Disabled | `data-disabled on the wrapper, disabled on the input` | Muted surface; no clear button. |
| Focus | – | The whole wrapper shows the Monarch focus ring while the input has focus. |

## Interaction behaviour

- Typing calls `onValueChange` at once and `onSearch` after `debounce` ms without typing (at once when `debounce` is 0).
- Enter calls `onSearch` immediately and cancels the waiting debounce.
- Clearing calls `onValueChange("")`, `onSearch("")` and `onClear`, and keeps focus in the field.
- With `shortcutKeys`, pressing the combination anywhere focuses the field and selects its text. Single-key shortcuts such as "/" are ignored while another field has focus.
- Controlled with `value` + `onValueChange`, or uncontrolled with `defaultValue`.

## Keyboard support

| Keys | Action |
| --- | --- |
| Enter | Runs the search now. |
| Escape | Clears the text and stops the event. On an empty field Escape is not handled and bubbles. Inside a Monarch Dialog or Popover, the overlay sees Escape first (Radix listens in the capture phase) and closes. |
| Tab | Moves to the clear button while there is text, then onward. |
| `shortcutKeys` | Focuses the field from anywhere on the page. |

## Accessibility

Role

searchbox (native input type="search")

Accessible name

A FieldLabel, or

aria-label

when the context makes the purpose clear ("Search documents"). The placeholder is not a label.

- The clear button is a real button named by `clearLabel` (default "Clear search").
- Icons, the spinner and the shortcut hint are `aria-hidden`; `aria-keyshortcuts` announces the shortcut instead.
- `aria-busy` is set while pending. Announce the result count yourself in a polite live region, as in the example.
- Wrap a page-level search in `<search>` or `role="search"` yourself; the component does not add a landmark, because filters inside cards should not create one.

## Responsive behaviour

- Fills its container; the icon, clear button and hint keep their size and the text field shrinks.
- `enterkeyhint="search"` shows a Search key on mobile keyboards.

## Theme support

- Colours, border and focus ring are Monarch semantic tokens, so light, dark and more-contrast themes apply automatically.
- The native WebKit cancel button is hidden so only the Monarch clear button shows.
- The clear button hover transition is removed under `prefers-reduced-motion`.

## Do and don't

### Do

- Use `debounce` (250–400 ms) when each search hits a server.
- Show `pending` from the moment the user types until results arrive.
- Announce the number of results in a polite live region.

### Don't

- Do not rely on the placeholder as the only label.
- Do not bind a single-key shortcut on pages where the user mostly types; prefer a modifier combination.
- Do not fetch in `onValueChange` when `onSearch` with `debounce` does it once per pause.

## Related components

- [Input](https://monarchui.dev/components/text-field/): Plain Input for text that is not a query.
- [Combobox](https://monarchui.dev/components/combobox/): Pick one value from a filtered list.
- [FilterBar](https://monarchui.dev/components/filter-bar/): A row of filters that usually starts with a SearchInput.
- [Kbd](https://monarchui.dev/components/kbd/): The shortcut hint is a Kbd.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Field](https://monarchui.dev/components/form-field/), [Spinner](https://monarchui.dev/components/spinner/), [Kbd](https://monarchui.dev/components/kbd/) |

Uses the `.mn-input` styles from text-field for the wrapper.

## Testing coverage

- Renders a labelled native searchbox.
- Debounces `onSearch` while reporting every keystroke through `onValueChange`.
- Enter searches at once and cancels the waiting debounce; no debounce searches on every change.
- The clear button clears, calls `onSearch("")` and `onClear`, and keeps focus.
- Escape clears; a second Escape on an empty field reaches the page.
- Controlled mode.
- Pending sets `aria-busy` and shows the spinner.
- Single-key and modifier shortcuts focus the field; single keys are ignored while typing elsewhere.
- Reads Field label, error and disabled state; no clear button when disabled.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery; Cross-browser axe: /components/search-input |

Tests: `packages/react/src/components/search/search-input`

## All example code

### Debounced, pending

```tsx
import { useState } from 'react';
import { SearchInput } from '@rookcom/monarch-react';

const FRUIT = ['Apple', 'Apricot', 'Banana', 'Blueberry', 'Cherry', 'Grape', 'Mango', 'Peach'];

function DebouncedDemo() {
  const [query, setQuery] = useState('');
  const [pending, setPending] = useState(false);
  const results = FRUIT.filter((f) => f.toLowerCase().includes(query.toLowerCase()));
  return (
    <div style={{ display: 'grid', gap: 8, width: '100%', maxWidth: 300 }}>
      <SearchInput
        aria-label="Search fruit"
        placeholder="Search fruit"
        debounce={300}
        pending={pending}
        onValueChange={() => setPending(true)}
        onSearch={(q) => {
          setQuery(q);
          setPending(false);
        }}
      />
      <p style={{ margin: 0, fontSize: 13 }} aria-live="polite">
        {results.length} {results.length === 1 ? 'result' : 'results'}
        {query ? ` for “${query}”` : ''}
      </p>
    </div>
  );
}

<DebouncedDemo />
```

### Shortcut, sizes and Field

```tsx
import { Field, FieldLabel, SearchInput } from '@rookcom/monarch-react';

<div style={{ display: 'grid', gap: 12, width: '100%', maxWidth: 300 }}>
  <SearchInput
    aria-label="Search the page"
    shortcut="/"
    shortcutKeys="/"
    placeholder="Search"
  />
  <SearchInput aria-label="Filter rows" size="sm" defaultValue="overdue" />
  <Field>
    <FieldLabel>Find a member</FieldLabel>
    <SearchInput placeholder="Name or email" />
  </Field>
</div>
```
