- Accordion
- AppShell
- Autocomplete
- Badge
- Blobatar
- BoringAvatar
- Button
- Calendar
- Checkbox
- DataGrid
- Date Picker
- Date Range Picker
- DateTime
- Dialog
- Drawer
- Dropdown
- Empty
- Error State
- Frame
- Input
- Input Amount
- Input Group
- Input OTP
- Input Password
- Input Phone
- Input Search
- Input Secret
- Loading
- PageContainer
- PageHeader
- Popover
- Select
- StatsCard
- Switch
- Tabs
- TextArea
- Toast
- Tooltip
Playground
"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:
- lucide-react
$ pnpm add lucide-reactUsage
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.
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
| Prop | Type / Description | Default |
|---|---|---|
| clearLabel | stringInputSearch: accessible name of the clear action. | "Clear search" |
| clearable | booleanInputSearch: shows a clear action for nonempty editable values. | false |
| defaultValue | stringInputSearch: initial uncontrolled search value. | undefined |
| disabled | booleanInputSearch: native disabled state. | false |
| label | stringInputSearch: visible associated label. | undefined |
| onChange | React.ChangeEventHandler<HTMLInputElement>InputSearch: native change callback, including clear action. | undefined |
| placeholder | stringInputSearch: native search placeholder. | undefined |
| readOnly | booleanInputSearch: prevents editing and clear action. | false |
| searchLabel | stringInputSearch: accessible name when no visible label is provided. | "Search" |
| value | stringInputSearch: 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>
);
}
);