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
⌘K
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
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | Controlled value. Leave out for an uncontrolled input. | |
| defaultValue | string | "" | Starting value for the uncontrolled form. |
| onValueChange | (value: string) => void | Fires with the new string on every change and when cleared. | |
| loading | boolean | false | Swaps the search icon for a spinner. |
| shortcut | string | Keyboard hint shown on the right, hidden while there is a value. | |
| clearLabel | string | "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