Inputs

Search Input

A styled search field with an icon, a clear button that appears once there's a value, an optional loading spinner, and a decorative shortcut hint.

Preview in your theme

Loading preview…

Dependencies
lucide-react
Registry dependencies
utils
"use client"

import * as React from "react"
import { Loader2, Search, X } from "lucide-react"
import { cn } from "@/lib/utils"

export interface SearchInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type"> {
  /** Swaps the clear button for a spinner in the same slot; sets aria-busy on the input. */
  loading?: boolean
  /** Controlled clear: consumer owns value, so clearing is their job — this just notifies.
   *  Uncontrolled: fired in addition to the component clearing its own DOM value. */
  onClear?: () => void
  /** Decorative key-combo hint (e.g. "⌘K") shown on the right when the field is empty and idle.
   *  Display only — binding the actual shortcut is the consumer's job. */

Installation

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

Prompt

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

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

  • 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.

On This Page