For the complete documentation index, see 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.
1
Support

Input Search

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

Playground

Live preview
Customize input-search
Customize
input-search-demo.tsx
"use client";

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

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

Installation

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

$ pnpm dlx shadcn@latest add https://vandor-ui.vercel.app/r/input-search.json

Dependencies

Installed automatically by the registry CLI. For manual setup:

Dependencies1 required packages
  • lucide-react
$ pnpm add lucide-react

Usage

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

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

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

export const InputSearchDemo = () => (
  <InputSearch
    label="Search components"
    placeholder="Try ‘calendar’"
    clearable
    defaultValue="input"
    className="max-w-sm"
  />
);

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

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

Component props, types, defaults, and descriptions
PropType / DescriptionDefault
clearLabelstring

InputSearch: accessible name of the clear action.

"Clear search"
clearableboolean

InputSearch: shows a clear action for nonempty editable values.

false
defaultValuestring

InputSearch: initial uncontrolled search value.

undefined
disabledboolean

InputSearch: native disabled state.

false
labelstring

InputSearch: visible associated label.

undefined
onChangeReact.ChangeEventHandler<HTMLInputElement>

InputSearch: native change callback, including clear action.

undefined
placeholderstring

InputSearch: native search placeholder.

undefined
readOnlyboolean

InputSearch: prevents editing and clear action.

false
searchLabelstring

InputSearch: accessible name when no visible label is provided.

"Search"
valuestring

InputSearch: controlled native search value.

uncontrolled

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

"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>
    );
  }
);