# Search Input (/docs/inputs/search-input)



<ComponentShowcase name="search-input" />

## Installation [#installation]

```bash
npx shadcn@latest add https://ui.zyeon.ai/r/search-input.json
```

## Prompt [#prompt]

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

```text
Build a React + TypeScript + Tailwind "SearchInput" component (lucide-react
Search/X/Loader2).

Contract
- Export a forwardRef component whose ref points at the inner <input>;
  props extend InputHTMLAttributes<HTMLInputElement> minus "type" (the
  component always renders type="search").
- Add loading?: boolean (default false) and shortcutHint?: string, plus
  onClear?: () => void.
- Fully supports both controlled (value + onChange) and uncontrolled
  (defaultValue) usage, same pattern as a PasswordInput: value/defaultValue/
  onChange pass straight through to the inner input, unmodified in shape.

Behavior
- A leading Search icon is always shown, decorative (aria-hidden).
- Track whether the field "has a value": in controlled mode read it
  straight from value every render (no extra state needed); in
  uncontrolled mode mirror it into internal state on every onChange
  (seeded once from defaultValue).
- When it has a value, a trailing icon button appears with
  aria-label="Clear search". Clicking it: in controlled mode, just call
  onClear() — the component never touches value itself, the consumer owns
  that state and re-renders with an empty value; in uncontrolled mode,
  set the underlying <input>'s DOM value to "" directly, flip the internal
  mirror to false, and also call onClear() if provided. Either way, focus
  returns to the input afterward.
- Escape, while the field has a value: preventDefault (so it doesn't rely
  on the browser's native type="search" escape-clear, which is
  inconsistent across engines) and run the exact same clear path as the
  button click.
- loading=true replaces whatever is in that trailing slot with a spinning
  Loader2 (motion-reduce: no spin, just static) and sets aria-busy on the
  input — the clear button and the shortcut hint both yield to it.
- shortcutHint (e.g. "⌘K") renders as a small kbd-styled hint in the
  trailing slot, but only when the field is both empty and not loading.
  It is pure display — this component does not bind any keyboard
  shortcut; wiring metaKey/ctrlKey + "k" to focus the input is the
  consumer's job (see Customization levers).
- disabled dims the field and disables the clear button.
- Hide the native type="search" cancel icon
  ([&::-webkit-search-cancel-button]:hidden) so there's never a second,
  browser-drawn clear control fighting the custom one.

Rendering & styling
- Outer field mimics the shadcn Input: flex h-9 items-center gap-2
  rounded-md border border-input bg-transparent px-3, focus-within:
  border-ring + focus-within:ring-ring/50 so the whole box focuses as one
  field; the inner <input> itself is borderless and bg-transparent.
- Icon / spinner / hint all read text-muted-foreground; the clear button
  is a ghost icon button (hover:bg-muted hover:text-foreground,
  focus-visible ring). The shortcut hint is a <kbd> with
  border-input + bg-muted, font-mono, pointer-events-none (display only).
- Semantic tokens only, no hardcoded hex/oklch. Merge the consumer
  className via cn() onto the field container.

Customization levers
- Debouncing: this component stays a dumb, uncontrolled-friendly field —
  don't debounce inside it. Pair it with the use-debounce-value hook in
  the consumer (debounce the value coming out of onChange) before firing
  a real request, and flip loading on/off around that request yourself.
- Shortcut binding recipe: shortcutHint only renders the hint. To make
  "⌘K" real, add a page-level keydown listener
  ((e.metaKey || e.ctrlKey) && e.key === "k") that preventDefaults and
  calls the forwarded ref's .focus() — completely decoupled from this
  component.
- Sizing: h-9 / px-3 / gap-2 are the density knobs on the container; the
  icon/button sizes (size-4 / size-5) scale alongside them.
- Icon swap: replace the leading Search icon with a different lucide icon
  for a themed variant (e.g. a command-bar look) — none of the clear/
  loading/hint logic depends on which icon leads.
```

## Concepts [#concepts]

<Mermaid
  chart="`flowchart TD
A[&#x22;User types&#x22;] --> B{&#x22;Controlled?&#x22;}
B -->|&#x22;yes&#x22;| C[&#x22;value prop is the source of truth&#x22;]
B -->|&#x22;no&#x22;| D[&#x22;mirror hasValue into internal state&#x22;]
C --> E{&#x22;hasValue?&#x22;}
D --> E
E -->|&#x22;yes&#x22;| F[&#x22;show Clear (X) button&#x22;]
E -->|&#x22;no&#x22;| G{&#x22;shortcutHint set?&#x22;}
G -->|&#x22;yes&#x22;| H[&#x22;show kbd hint&#x22;]
G -->|&#x22;no&#x22;| I[&#x22;show nothing&#x22;]
J[&#x22;loading = true&#x22;] --> K[&#x22;swap trailing slot to spinner<br/>+ aria-busy on input&#x22;]
L[&#x22;Click Clear / press Escape&#x22;] --> M{&#x22;Controlled?&#x22;}
M -->|&#x22;yes&#x22;| N[&#x22;call onClear() — consumer clears value&#x22;]
M -->|&#x22;no&#x22;| O[&#x22;set input.value = ''<br/>+ mirror state to false&#x22;]
N --> P[&#x22;focus returns to input&#x22;]
O --> P`"
/>

* **Clear button on value** — the trailing X only exists once there's something to clear, so an empty field never shows a control with nothing to do.
* **Controlled vs uncontrolled mirroring** — controlled mode reads `hasValue` straight off `value` every render; uncontrolled mode keeps a small internal mirror updated on `onChange`, exactly so the clear button knows when to appear without the component secretly owning the value.
* **Honest clearing** — in controlled mode the component never mutates `value` itself, it only calls `onClear()` and trusts the consumer to update their own state; in uncontrolled mode it clears the DOM node directly since there's no external state to wait for.
* **Loading swaps the trailing slot** — a spinner takes over the exact spot the clear button or shortcut hint would occupy, so the field never grows a second icon while a search is in flight.
* **Escape mirrors the button** — pressing Escape runs through the identical clear path as clicking the X, instead of relying on browser-specific native `type="search"` clearing behavior.
* **Shortcut hint is decorative only** — `shortcutHint` renders a `<kbd>` label, nothing more; binding the real key combo to focus the field is left to the page, which is where global shortcuts belong anyway.
