# Search Input

A search icon that springs open into a field. Escape clears and collapses it.

**Category:** Inputs  
**Exports:** `SearchInput`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/search-input.json
```
The file lands at `components/ui/search-input.tsx`; import from `@/components/ui/search-input`.

## API — Search Input
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `value / onValueChange` | `string / (v: string) => void` | — |  |
| `onSearch` | `(value: string) => void` | — |  |
| `width` | `number` | `260` |  |

## Example — Search Input

```tsx
import * as React from "react";
import { SearchInput } from "@/components/ui/search-input";

export default function SearchInputDemo({ placeholder = "Search docs…", width = 260 }: { placeholder?: string; width?: number }) {
  const [last, setLast] = React.useState("");
  return (
    <div className="flex flex-col items-center gap-4">
      <SearchInput placeholder={placeholder} width={width} onSearch={setLast} />
      <p className="h-5 text-sm text-muted-foreground">{last ? `Searched for “${last}”` : "Click the icon, type, press Enter."}</p>
    </div>
  );
}
```

## Source — components/ui/search-input.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

export type SearchInputProps = {
  value?: string;
  onValueChange?: (value: string) => void;
  onSearch?: (value: string) => void;
  placeholder?: string;
  /** Width when open, in px. */
  width?: number;
  className?: string;
};

export function SearchInput({ value: valueProp, onValueChange, onSearch, placeholder = "Search…", width = 260, className }: SearchInputProps) {
  const reduce = useReducedMotion();
  const [inner, setInner] = React.useState("");
  const value = valueProp ?? inner;
  const [open, setOpen] = React.useState(false);
  const inputRef = React.useRef<HTMLInputElement>(null);

  const set = (v: string) => {
    if (valueProp === undefined) setInner(v);
    onValueChange?.(v);
  };

  React.useEffect(() => {
    if (open) inputRef.current?.focus();
  }, [open]);

  return (
    <motion.form
      role="search"
      onSubmit={(e) => {
        e.preventDefault();
        onSearch?.(value);
      }}
      initial={false}
      animate={{ width: open ? width : 40 }}
      transition={reduce ? { duration: 0 } : { type: "spring", stiffness: 420, damping: 34 }}
      className={cn("relative flex h-10 items-center overflow-hidden rounded-full border bg-background", open && "ring-[3px] ring-ring/20", className)}
    >
      <button
        type={open ? "submit" : "button"}
        aria-label={open ? "Search" : "Open search"}
        onClick={() => !open && setOpen(true)}
        className="grid size-10 shrink-0 cursor-pointer place-items-center rounded-full text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring"
      >
        <svg viewBox="0 0 24 24" className="size-[18px]" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden>
          <circle cx="11" cy="11" r="7" />
          <path d="m20 20-3.5-3.5" />
        </svg>
      </button>
      <input
        ref={inputRef}
        type="search"
        aria-label={placeholder}
        tabIndex={open ? 0 : -1}
        value={value}
        placeholder={placeholder}
        onChange={(e) => set(e.target.value)}
        onKeyDown={(e) => {
          if (e.key === "Escape") {
            set("");
            setOpen(false);
          }
        }}
        onBlur={() => !value && setOpen(false)}
        className="h-full min-w-0 flex-1 bg-transparent pr-2 text-base outline-none placeholder:text-muted-foreground sm:text-sm [&::-webkit-search-cancel-button]:hidden"
      />
      <AnimatePresence>
        {open && value && (
          <motion.button
            type="button"
            aria-label="Clear search"
            onClick={() => {
              set("");
              inputRef.current?.focus();
            }}
            initial={{ opacity: 0, scale: 0.5 }}
            animate={{ opacity: 1, scale: 1 }}
            exit={{ opacity: 0, scale: 0.5 }}
            className="mr-1.5 grid size-7 shrink-0 cursor-pointer place-items-center rounded-full bg-secondary text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
          >
            <svg viewBox="0 0 24 24" className="size-3" fill="none" stroke="currentColor" strokeWidth="2.6" strokeLinecap="round" aria-hidden>
              <path d="M6 6l12 12M18 6 6 18" />
            </svg>
          </motion.button>
        )}
      </AnimatePresence>
    </motion.form>
  );
}
```