Combobox
An input with a filterable dropdown list — local options with client-side filtering, or async remote search with debounced fetching and scroll-to-load-more
Installation
- $npx litefy@latest add combobox
- $pnpm dlx litefy@latest add combobox
- $yarn dlx litefy@latest add combobox
- $bun --bun litefy@latest add combobox
Usage
Type to filter local string options; focus never leaves the input — Combobox is a composition of Picker and List, auto-resolved by the CLI install.
"use client";
import { Combobox } from "@/ui";
const countries = [
"China",
"United States",
"Japan",
"Germany",
"France",
"United Kingdom",
"Canada",
"Australia",
"Italy",
"Brazil",
];
export default function Demo() {
return <Combobox options={countries} placeholder="Select a country" />;
}
Invalid State
invalid shows the danger-border state; wire it to your own validation.
"use client";
import { useState } from "react";
import { Combobox } from "@/ui";
const countries = [
"China",
"United States",
"Japan",
"Germany",
"France",
"United Kingdom",
"Canada",
"Australia",
"Italy",
"Brazil",
];
export default function Demo() {
const [value, setValue] = useState("");
const invalid = value.length > 0 && !countries.includes(value);
return (
<div className="flex flex-col gap-4 w-72">
<Combobox
options={countries}
placeholder="Choose a country"
value={value}
onValueChange={setValue}
invalid={invalid}
/>
{invalid && (
<span className="text-danger text-sm">
Pick a country from the list — free text is not allowed
</span>
)}
</div>
);
}
Empty
empty swaps the default "No data" node for any React node.
"use client";
import { SearchX } from "lucide-react";
import { Combobox } from "@/ui";
const frameworks = ["React", "Vue", "Svelte", "Solid", "Preact"];
export default function Demo() {
return (
<div className="w-72">
<Combobox
options={frameworks}
placeholder="Search a framework"
empty={
<div className="flex flex-col items-center gap-1.5 py-4">
<SearchX className="size-5" />
<span>No framework matches your search</span>
</div>
}
/>
</div>
);
}
Async Search
Pass fetcher instead of options for large or remote datasets; fetcher takes precedence when both are provided.
"use client";
import { Combobox, type ComboboxFetcher } from "@/ui";
const fetchAsyncOptions: ComboboxFetcher = async ({ page, size, keyword }) => {
await new Promise((resolve) => setTimeout(resolve, 200));
const totalItems = 1000;
const allItems = Array.from({ length: totalItems }, (_, i) => `Item ${i + 1}`);
const filtered = keyword
? allItems.filter((item) => item.toLowerCase().includes(keyword.toLowerCase()))
: allItems;
const start = (page - 1) * size;
const paged = filtered.slice(start, start + size);
return { list: paged, total: filtered.length };
};
export default function Demo() {
return <Combobox fetcher={fetchAsyncOptions} placeholder="Search items" />;
}
API Reference
Combobox
| Prop | Type | Default | Description |
|---|---|---|---|
options | string[] | - | Local options for client-side filtering |
fetcher | ({ page, size, keyword }) => Promise<{ list: string[]; total: number }> | - | Remote fetcher for async mode |
defaultValue / value | string | "" | Input text (uncontrolled / controlled) |
onValueChange | (value: string) => void | - | Fired on typing and on selection |
invalid | boolean | false | Invalid state styling on the input |
empty | React.ReactNode | "No data" / "Loading..." | Empty-state node rendered in the list |
pageSize | number | 20 | Page size for async fetching |
debounceMs | number | 300 | Debounce delay for async keyword search |
className | ClassNameValue | - | Custom classes, applied to the input wrapper |
classNames | { panel? / item? } | - | Custom classes for the panel and list items |
Chip Group
A primitive component that automatically collapses overflowing chips, exposing the hidden set via renderMore / onOverflowChange for consumer-side composition
Command
A dialog-based command palette — a trigger (or application hotkey) opens a dialog with a search input on top and a filtered, keyboard-navigable list below.