# Input Search

A search field with an optional clear action and native input events.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
"use client";

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

export function InputSearchDemo() {
  return (
    <InputSearch
        label={"Search"}
        placeholder={"Search components"}
        clearable
        defaultValue="Calendar"
    />
  );
}
```

## Installation [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
npx shadcn@latest add https://vandor-ui.vercel.app/r/input-search.json
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install lucide-react
```

## Usage [#usage]

```tsx
import { InputSearch } from "@/components/ui/input-search";

<InputSearch label="Search" placeholder="Search components" clearable />;
```

Clearing is opt-in. Controlled values use the native `value` and `onChange` contract.

## Examples [#examples]

This uncontrolled example starts populated so the clear action and focus restoration can be tried.

<ComponentPreview name="input-search-demo" title="input-search-demo.tsx">
  <InputSearchDemo />
</ComponentPreview>

### Optional Storybook Stories [#optional-storybook-stories]

Install Input Search first, then run `npx shadcn@latest add https://vandor-ui.vercel.app/r/input-search-stories.json`. Stories cover clearable and empty queries, controlled search, label styles, and disabled/read-only/invalid states. Search stays local; no data service is required.

Stories install beside Input Search under your configured UI alias and use relative imports. Use your existing matching `@storybook/react` packages (verified with 10.6.1), discover `**/*.stories.tsx`, and load your global Tailwind/theme stylesheet. No addons, extra packages, core overwrites, or Storybook config edits are required. Default-value Controls remount uncontrolled examples; Controlled owns local state. Storybook UI is not verified because no server exists here.

## Behavior & Accessibility [#behavior--accessibility]

The clear button is available only for a nonempty editable value, clears the field, and restores focus to it. Search debouncing and requests remain the caller's responsibility.

## Props [#props]

| Prop         | Type                                        | Default        | Description                                                     |
| ------------ | ------------------------------------------- | -------------- | --------------------------------------------------------------- |
| clearLabel   | string                                      | "Clear search" | InputSearch: accessible name of the clear action.               |
| clearable    | boolean                                     | false          | InputSearch: shows a clear action for nonempty editable values. |
| defaultValue | string                                      | undefined      | InputSearch: initial uncontrolled search value.                 |
| disabled     | boolean                                     | false          | InputSearch: native disabled state.                             |
| label        | string                                      | undefined      | InputSearch: visible associated label.                          |
| onChange     | React.ChangeEventHandler\<HTMLInputElement> | undefined      | InputSearch: native change callback, including clear action.    |
| placeholder  | string                                      | undefined      | InputSearch: native search placeholder.                         |
| readOnly     | boolean                                     | false          | InputSearch: prevents editing and clear action.                 |
| searchLabel  | string                                      | "Search"       | InputSearch: accessible name when no visible label is provided. |
| value        | string                                      | uncontrolled   | InputSearch: controlled native search value.                    |

The reference lists component-specific props and selected native props, not the complete React API. Other native attributes are forwarded to the rendered element.

## Source [#source]

```tsx
"use client";

import { cn } from "cn";
import { Search, X } from "lucide-react";
import * as React from "react";

import { Button } from "./button";
import { Input } from "./input";
import type { InputProps } from "./input";

export type InputSearchProps = Omit<InputProps, "type" | "icon"> & {
  clearable?: boolean;
  searchLabel?: string;
  clearLabel?: string;
};

export const InputSearch = React.forwardRef<HTMLInputElement, InputSearchProps>(
  function InputSearch(
    {
      clearable = false,
      clearLabel = "Clear search",
      disabled,
      readOnly,
      searchLabel = "Search",
      value,
      defaultValue,
      onChange,
      className,
      ...props
    },
    forwardedRef
  ) {
    const inputRef = React.useRef<HTMLInputElement>(null);
    React.useImperativeHandle(
      forwardedRef,
      () => inputRef.current as HTMLInputElement
    );
    const [uncontrolled, setUncontrolled] = React.useState(
      String(defaultValue ?? "")
    );
    const current = String(value === undefined ? uncontrolled : (value ?? ""));
    return (
      <div data-slot="search-input" className="relative min-w-0">
        <Input
          {...props}
          ref={inputRef}
          type="search"
          aria-label={props["aria-label"] ?? props.label ?? searchLabel}
          icon={<Search aria-label={searchLabel} size={16} />}
          disabled={disabled}
          readOnly={readOnly}
          value={current}
          onChange={(event) => {
            if (value === undefined) {
              setUncontrolled(event.currentTarget.value);
            }
            onChange?.(event);
          }}
          className={cn(
            "pr-10 [&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none",
            className
          )}
        />
        {clearable && current && !disabled && !readOnly ? (
          <Button
            type="button"
            variant="ghost"
            size="icon-sm"
            aria-label={clearLabel}
            className="absolute top-1/2 right-1 -translate-y-1/2"
            onClick={() => {
              const input = inputRef.current;
              if (!input) {
                return;
              }
              const setter = Object.getOwnPropertyDescriptor(
                HTMLInputElement.prototype,
                "value"
              )?.set;
              setter?.call(input, "");
              setUncontrolled("");
              input.dispatchEvent(new Event("input", { bubbles: true }));
              input.focus();
            }}
          >
            <X aria-hidden="true" />
          </Button>
        ) : null}
      </div>
    );
  }
);

```
