Search Input
Compact search field with a leading magnifier and value-gated clear action.
Search Input is a controlled text field for query bars and filter rows. It keeps a magnifier visible at the leading edge and only shows the clear affordance when there is content to reset.
When To Use
- Add fast keyword filtering to toolbars, data lists, and command-like headers.
- Keep query state fully controlled by your page, route params, or external filters.
- Use it when you want a predictable clear action that always returns focus to the input.
- Prefer plain
Inputwhen you do not need search semantics or a built-in clear affordance.
Interactions
| Event | Behavior |
|---|---|
| Type in the field | Calls onValueChange(nextValue) on each change. |
| Value becomes non-empty | Shows trailing clear button with clearLabel for a11y. |
| Press clear | Calls onClear when provided, otherwise falls back to onValueChange(''). |
| Clear action completes | Input is focused again so users can continue typing immediately. |
loading={true} | Leading magnifier swaps to a spinner; input stays editable unless disabled. |
Debounce and remote query wiring stay in callers such as DataListSearchInput — this primitive does not debounce.
Installing
pnpm dlx shadcn@latest add https://ui.isaacfei.com/r/search-input.jsonnpx shadcn@latest add https://ui.isaacfei.com/r/search-input.jsonyarn dlx shadcn@latest add https://ui.isaacfei.com/r/search-input.jsonbun x shadcn@latest add https://ui.isaacfei.com/r/search-input.jsonOr with a namespace: npx shadcn@latest add @f-ui/search-input.
The CLI installs lucide-react, and pulls button and input from the default shadcn registry.
Usage
import { SearchInput } from '@/components/f-ui/search-input/search-input';
<SearchInput
value={query}
onValueChange={setQuery}
clearLabel="Clear search"
placeholder="Search..."
/>Examples
Toolbar Query
Use the default controlled pattern for list or table filters. Type to update the query and clear to reset quickly.
Current query: none
"use client";
import { useState } from "react";
import { SearchInput } from "@/components/f-ui/search-input/search-input";
export function SearchInputDemo() {
const [query, setQuery] = useState("");
return (
<div className="max-w-sm space-y-3">
<SearchInput
value={query}
onValueChange={setQuery}
clearLabel="Clear search"
placeholder="Search orders..."
/>
<p className="text-muted-foreground text-xs">
Current query:{" "}
<span className="text-foreground font-medium tabular-nums">
{query || "none"}
</span>
</p>
</div>
);
}Controlled Clear Handler
Pass a custom onClear when you need side effects (analytics, counters, or filter resets) alongside clearing the value.
Clear pressed: 0
"use client";
import { useState } from "react";
import { SearchInput } from "@/components/f-ui/search-input/search-input";
import { Button } from "@/components/ui/button";
export function SearchInputControlledDemo() {
const [query, setQuery] = useState("status:open");
const [clearCount, setClearCount] = useState(0);
return (
<div className="max-w-sm space-y-3">
<SearchInput
value={query}
onValueChange={setQuery}
onClear={() => {
setQuery("");
setClearCount((count) => count + 1);
}}
clearLabel="Clear search"
placeholder="Type to filter..."
/>
<div className="flex gap-2">
<Button type="button" variant="outline" onClick={() => setQuery("error")}>
Set "error"
</Button>
<Button type="button" variant="outline" onClick={() => setQuery("")}>
Reset
</Button>
</div>
<p className="text-muted-foreground text-xs">
Clear pressed:{" "}
<span className="text-foreground font-medium tabular-nums">
{clearCount}
</span>
</p>
</div>
);
}Remote Loading
Pass loading while a flushed remote query runs. The magnifier becomes a spinner in the same slot; the clear button stays available when there is a value.
"use client";
import { useState } from "react";
import { SearchInput } from "@/components/f-ui/search-input/search-input";
import { Button } from "@/components/ui/button";
export function SearchInputLoadingDemo() {
const [query, setQuery] = useState("orders");
const [loading, setLoading] = useState(false);
function simulateFetch() {
setLoading(true);
window.setTimeout(() => setLoading(false), 1200);
}
return (
<div className="max-w-sm space-y-3">
<SearchInput
value={query}
onValueChange={setQuery}
clearLabel="Clear search"
loading={loading}
loadingLabel="Searching…"
placeholder="Search orders..."
/>
<Button type="button" variant="outline" onClick={simulateFetch}>
Simulate remote search
</Button>
</div>
);
}Composition
SearchInput
├── SearchIcon or Loader2 (leading; spinner when loading)
├── Input type="search"
└── FieldSuffixAction clear (trailing, only when value is non-empty)API Reference
Props
| Prop | Type | Default |
|---|---|---|
value | string | — |
onValueChange | (value: string) => void | — |
onClear | () => void | onValueChange('') |
clearLabel | string | — |
loading | boolean | false |
loadingLabel | string | "Searching…" |
placeholder | string | — |
size | 'sm' | 'default' | 'sm' |
disabled | boolean | false |
id | string | — |
className | string | — |
inputProps | Input props (without value/change/type/disabled/placeholder/id/className) | — |