f-ui
Components

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 Input when you do not need search semantics or a built-in clear affordance.

Interactions

EventBehavior
Type in the fieldCalls onValueChange(nextValue) on each change.
Value becomes non-emptyShows trailing clear button with clearLabel for a11y.
Press clearCalls onClear when provided, otherwise falls back to onValueChange('').
Clear action completesInput 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.json
npx shadcn@latest add https://ui.isaacfei.com/r/search-input.json
yarn dlx shadcn@latest add https://ui.isaacfei.com/r/search-input.json
bun x shadcn@latest add https://ui.isaacfei.com/r/search-input.json

Or 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

PropTypeDefault
valuestring
onValueChange(value: string) => void
onClear() => voidonValueChange('')
clearLabelstring
loadingbooleanfalse
loadingLabelstring"Searching…"
placeholderstring
size'sm' | 'default''sm'
disabledbooleanfalse
idstring
classNamestring
inputPropsInput props (without value/change/type/disabled/placeholder/id/className)

On this page