Skip to content
goodtabs UI
BrowseBrowse components

Search input

A search field with a leading icon, a clear button and a keyboard hint.

npx shadcn@latest add https://ui.goodtabs.co/r/search-input.json

With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/search-input

The hint hides as soon as there is a value and a clear button takes its place.

Loading

The search icon becomes a spinner while results load. Nothing else moves.

Controlled, in a toolbar

7 of 7
  • Inter
  • Geist
  • Host Grotesk
  • Fragment Mono
  • Instrument Serif
  • Space Grotesk
  • Manrope

Controlled value with a live results count next to it.

Usage

import { SearchInput } from "@/components/ui/search-input"

<SearchInput placeholder="Search resources" shortcut="⌘K" onValueChange={setQuery} />

Props

PropTypeDefaultDescription
valuestringControlled value. Leave out for an uncontrolled input.
defaultValuestring""Starting value for the uncontrolled form.
onValueChange(value: string) => voidFires with the new string on every change and when cleared.
loadingbooleanfalseSwaps the search icon for a spinner.
shortcutstringKeyboard hint shown on the right, hidden while there is a value.
clearLabelstring"Clear search"Accessible name for the clear button.

Source

search-input/search-input.tsx
"use client";

import * as React from "react";
import { Loader2Icon, SearchIcon, XIcon } from "lucide-react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";

type SearchInputProps = Omit<React.ComponentProps<typeof Input>, "type" | "value" | "defaultValue" | "onChange"> & {
  /** Controlled value. Leave out for an uncontrolled input. */
  value?: string;
  /** Starting value for the uncontrolled form. */
  defaultValue?: string;
  /** Fires with the new string on every change and when cleared. */
  onValueChange?: (value: string) => void;
  /** Swaps the search icon for a spinner while results load. */
  loading?: boolean;
  /** Keyboard hint shown on the right, for example "⌘K" or "/". Hidden while there is a value. */
  shortcut?: string;
  /** Accessible name for the clear button. */
  clearLabel?: string;
};

/**
 * A search field with a leading icon, a clear button and an optional keyboard hint.
 * Works controlled or uncontrolled. Escape clears the value.
 */
export function SearchInput({
  value,
  defaultValue = "",
  onValueChange,
  loading = false,
  shortcut,
  clearLabel = "Clear search",
  className,
  placeholder = "Search",
  disabled,
  ...props
}: SearchInputProps) {
  const [inner, setInner] = React.useState(defaultValue);
  const controlled = value !== undefined;
  const current = controlled ? value : inner;
  const inputRef = React.useRef<HTMLInputElement>(null);

  const update = (next: string) => {
    if (!controlled) setInner(next);
    onValueChange?.(next);
  };

  const clear = () => {
    update("");
    inputRef.current?.focus();
  };

  const Icon = loading ? Loader2Icon : SearchIcon;

  return (
    <div className={cn("relative flex items-center", className)}>
      <Icon
        aria-hidden="true"
        className={cn(
          "pointer-events-none absolute left-2.5 size-4 text-muted-foreground",
          loading && "animate-spin motion-reduce:animate-none",
        )}
      />
      <Input
        ref={inputRef}
        type="search"
        role="searchbox"
        value={current}
        onChange={(e) => update(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === "Escape" && current) {
            e.preventDefault();
            clear();
          }
        }}
        placeholder={placeholder}
        disabled={disabled}
        aria-busy={loading || undefined}
        className={cn("pl-8 [&::-webkit-search-cancel-button]:hidden", current || shortcut ? "pr-9" : "")}
        {...props}
      />
      {current ? (
        <button
          type="button"
          onClick={clear}
          disabled={disabled}
          aria-label={clearLabel}
          className="absolute right-1.5 inline-grid size-6 place-items-center rounded-sm text-muted-foreground transition-colors duration-100 ease-out hover:bg-muted hover:text-foreground disabled:pointer-events-none disabled:opacity-50"
        >
          <XIcon className="size-3.5" aria-hidden="true" />
        </button>
      ) : shortcut ? (
        <kbd
          aria-hidden="true"
          className="pointer-events-none absolute right-2 inline-grid h-5 min-w-5 place-items-center rounded-sm border bg-background px-1 font-mono text-xs leading-none text-muted-foreground"
        >
          {shortcut}
        </kbd>
      ) : null}
    </div>
  );
}

For AI agents

Hand your agent this page, the markdown at /components/search-input/prompt.md, or the Copy AI prompt button above.

When to use

  • Filtering a list, table or grid as the person types
  • A site or app search box that opens with a keyboard shortcut
  • Anywhere a plain input would need a search icon, a clear button and a hint bolted on

When not to use

  • A full command palette with results and keyboard navigation: build that on a Command component
  • Forms where the field is a plain text value, not a query

Agent notes

  • It extends shadcn's Input, so size, disabled and every input prop pass through
  • Uncontrolled by default; pass value plus onValueChange to control it
  • The shortcut prop only draws the hint, wire the actual keyboard shortcut yourself
  • Escape clears the value and keeps focus in the field; the native search cancel button is hidden