Combobox
An input combined with a list of predefined items to select. Built with Base UI and Tailwind CSS. Copy-paste ready.
"use client";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Combobox items={items}>
<ComboboxInput aria-label="Select a item" placeholder="Select a item…" />
<ComboboxPopup>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/combobox
Usage
Single Selection
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox"const items = [
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "orange", label: "Orange" },
{ value: "grape", label: "Grape" },
]
<Combobox items={items}>
<ComboboxInput placeholder="Select an item..." />
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => <ComboboxItem key={item.value} value={item}>{item.label}</ComboboxItem>}
</ComboboxList>
</ComboboxPopup>
</Combobox>Multiple Selection
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxValue,
} from "@/components/ui/combobox"const items = [
{ value: "apple", label: "Apple" },
{ value: "banana", label: "Banana" },
{ value: "orange", label: "Orange" },
{ value: "grape", label: "Grape" },
]
<Combobox items={items} multiple>
<ComboboxChips>
<ComboboxValue>
{(value: { value: string; label: string }[]) => (
<>
{value?.map((item) => (
<ComboboxChip key={item.value} aria-label={item.value}>
{item.label}
</ComboboxChip>
))}
<ComboboxInput
placeholder={value.length > 0 ? undefined : "Select a Select an item..."}
aria-label="Select an item"
/>
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => <ComboboxItem value={item}>{item.label}</ComboboxItem>}
</ComboboxList>
</ComboboxPopup>
</Combobox>API Reference
The ComboboxInput component extends the original Base UI component with a few extra props:
| Prop | Type | Default | Description |
|---|---|---|---|
size | "sm" | "default" | "lg" | "default" | The size variant of the input field. |
showTrigger | boolean | true | Whether to display a trigger button (chevron icon) on the right side of the input. |
showClear | boolean | false | Whether to display a clear button (X icon) on the right side of the input when there is a value. |
Examples
Disabled
Use the disabled prop to prevent interaction. A defaultValue can still pre-populate the field.
"use client";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Combobox defaultValue={items[2]} disabled items={items}>
<ComboboxInput
aria-label="Select an item"
placeholder="Select an item…"
/>
<ComboboxPopup>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Sizes
Use the size prop on ComboboxInput to render sm or lg variants alongside the default.
"use client";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<>
<Combobox items={items}>
<ComboboxInput
aria-label="Select an item"
placeholder="Select an item..."
size="sm"
/>
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<Combobox items={items}>
<ComboboxInput
aria-label="Select an item"
placeholder="Select an item..."
size="lg"
/>
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
</>
);
}
With Label
Pair ComboboxInput with a Label via useId to give the field an accessible name.
"use client";
import { useId } from "react";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
import { Label } from "@/components/ui/label";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
const id = useId();
return (
<Combobox items={items}>
<div className="flex flex-col items-start gap-2">
<Label htmlFor={id}>Fruits</Label>
<ComboboxInput
aria-label="Select an item"
id={id}
placeholder="Select an item..."
/>
</div>
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Auto Highlight
The autoHighlight prop pre-focuses the first matching option as the user types.
"use client";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Combobox autoHighlight items={items}>
<ComboboxInput
aria-label="Select an item"
placeholder="Select an item..."
/>
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
With Clear Button
Set showClear on ComboboxInput to render an ✕ button that resets the selection.
"use client";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Combobox items={items}>
<ComboboxInput
aria-label="Select a item"
placeholder="Select a item…"
showClear
/>
<ComboboxPopup>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
With Groups
Use ComboboxGroup, ComboboxGroupLabel, ComboboxCollection, and ComboboxSeparator to organise items into labelled sections.
"use client";
import { Fragment } from "react";
import {
Combobox,
ComboboxCollection,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxSeparator,
} from "@/components/ui/combobox";
// Grouped items demo
type Tag = { id: string; label: string; group: "Status" | "Priority" | "Team" };
type TagGroup = { value: string; items: Tag[] };
const tagsData: Tag[] = [
// Status
{ group: "Status", id: "s-open", label: "Open" },
{ group: "Status", id: "s-in-progress", label: "In progress" },
{ group: "Status", id: "s-blocked", label: "Blocked" },
{ group: "Status", id: "s-resolved", label: "Resolved" },
{ group: "Status", id: "s-closed", label: "Closed" },
// Priority
{ group: "Priority", id: "p-low", label: "Low" },
{ group: "Priority", id: "p-medium", label: "Medium" },
{ group: "Priority", id: "p-high", label: "High" },
{ group: "Priority", id: "p-urgent", label: "Urgent" },
// Team
{ group: "Team", id: "t-design", label: "Design" },
{ group: "Team", id: "t-frontend", label: "Frontend" },
{ group: "Team", id: "t-backend", label: "Backend" },
{ group: "Team", id: "t-devops", label: "DevOps" },
{ group: "Team", id: "t-qa", label: "QA" },
{ group: "Team", id: "t-mobile", label: "Mobile" },
{ group: "Team", id: "t-data", label: "Data" },
{ group: "Team", id: "t-security", label: "Security" },
{ group: "Team", id: "t-platform", label: "Platform" },
{ group: "Team", id: "t-infra", label: "Infrastructure" },
{ group: "Team", id: "t-product", label: "Product" },
{ group: "Team", id: "t-marketing", label: "Marketing" },
{ group: "Team", id: "t-sales", label: "Sales" },
{ group: "Team", id: "t-support", label: "Support" },
{ group: "Team", id: "t-research", label: "Research" },
{ group: "Team", id: "t-content", label: "Content" },
{ group: "Team", id: "t-analytics", label: "Analytics" },
{ group: "Team", id: "t-operations", label: "Operations" },
{ group: "Team", id: "t-finance", label: "Finance" },
{ group: "Team", id: "t-hr", label: "HR" },
{ group: "Team", id: "t-legal", label: "Legal" },
{ group: "Team", id: "t-growth", label: "Growth" },
{ group: "Team", id: "t-partner", label: "Partner" },
{ group: "Team", id: "t-community", label: "Community" },
{ group: "Team", id: "t-docs", label: "Docs" },
{ group: "Team", id: "t-l10n", label: "Localization" },
{ group: "Team", id: "t-a11y", label: "Accessibility" },
{ group: "Team", id: "t-sre", label: "SRE" },
{ group: "Team", id: "t-release", label: "Release" },
{ group: "Team", id: "t-architecture", label: "Architecture" },
{ group: "Team", id: "t-ux", label: "UX" },
{ group: "Team", id: "t-ui", label: "UI" },
{ group: "Team", id: "t-management", label: "Management" },
];
function groupTags(tags: Tag[]): TagGroup[] {
const groups: Record<string, Tag[]> = {};
for (const tag of tags) {
if (!groups[tag.group]) {
groups[tag.group] = [];
}
groups[tag.group]?.push(tag);
}
const order: Array<TagGroup["value"]> = ["Status", "Priority", "Team"];
return order.map((value) => ({ items: groups[value] ?? [], value }));
}
const groupedTags: TagGroup[] = groupTags(tagsData);
export default function Particle() {
return (
<Combobox items={groupedTags}>
<div className="flex flex-col items-start gap-2">
<ComboboxInput aria-label="Search tags" placeholder="e.g. feature" />
</div>
<ComboboxPopup>
<ComboboxEmpty>No tags found.</ComboboxEmpty>
<ComboboxList>
{(group: TagGroup) => (
<Fragment key={group.value}>
<ComboboxGroup items={group.items}>
<ComboboxGroupLabel>{group.value}</ComboboxGroupLabel>
<ComboboxCollection>
{(tag: Tag) => (
<ComboboxItem key={tag.id} value={tag}>
{tag.label}
</ComboboxItem>
)}
</ComboboxCollection>
</ComboboxGroup>
{group.value !== "Team" && <ComboboxSeparator />}
</Fragment>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Multiple Selection
Set multiple on Combobox and wrap the input with ComboboxChips to render each selected value as a removable chip.
"use client";
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxValue,
} from "@/components/ui/combobox";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Combobox defaultValue={[items[0], items[4]]} items={items} multiple>
<ComboboxChips>
<ComboboxValue>
{(value: { value: string; label: string }[]) => (
<>
{value?.map((item) => (
<ComboboxChip aria-label={item.label} key={item.value}>
{item.label}
</ComboboxChip>
))}
<ComboboxChipsInput
aria-label="Select a item"
placeholder={value.length > 0 ? undefined : "Select a item..."}
/>
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxPopup>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
With Search Icon
Pass any icon to the startAddon prop of ComboboxInput to decorate the leading edge of the field.
"use client";
import { SearchIcon } from "lucide-react";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Combobox items={items}>
<ComboboxInput
aria-label="Search items"
placeholder="Search items…"
startAddon={<SearchIcon />}
/>
<ComboboxPopup>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
With Trigger Button
Render a ComboboxTrigger as a Button and move the search input inside ComboboxPopup for a select-style layout.
"use client";
import { ChevronsUpDownIcon, SearchIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
interface Country {
code: string;
value: string | null;
continent: string;
label: string;
}
const countries: Country[] = [
{ code: "", continent: "", label: "Select country", value: null },
{ code: "af", continent: "Asia", label: "Afghanistan", value: "afghanistan" },
{ code: "al", continent: "Europe", label: "Albania", value: "albania" },
{ code: "dz", continent: "Africa", label: "Algeria", value: "algeria" },
{ code: "ad", continent: "Europe", label: "Andorra", value: "andorra" },
{ code: "ao", continent: "Africa", label: "Angola", value: "angola" },
{
code: "ar",
continent: "South America",
label: "Argentina",
value: "argentina",
},
{ code: "am", continent: "Asia", label: "Armenia", value: "armenia" },
{ code: "au", continent: "Oceania", label: "Australia", value: "australia" },
{ code: "at", continent: "Europe", label: "Austria", value: "austria" },
{ code: "az", continent: "Asia", label: "Azerbaijan", value: "azerbaijan" },
{
code: "bs",
continent: "North America",
label: "Bahamas",
value: "bahamas",
},
{ code: "bh", continent: "Asia", label: "Bahrain", value: "bahrain" },
{ code: "bd", continent: "Asia", label: "Bangladesh", value: "bangladesh" },
{
code: "bb",
continent: "North America",
label: "Barbados",
value: "barbados",
},
{ code: "by", continent: "Europe", label: "Belarus", value: "belarus" },
{ code: "be", continent: "Europe", label: "Belgium", value: "belgium" },
{ code: "bz", continent: "North America", label: "Belize", value: "belize" },
{ code: "bj", continent: "Africa", label: "Benin", value: "benin" },
{ code: "bt", continent: "Asia", label: "Bhutan", value: "bhutan" },
{
code: "bo",
continent: "South America",
label: "Bolivia",
value: "bolivia",
},
{
code: "ba",
continent: "Europe",
label: "Bosnia and Herzegovina",
value: "bosnia-and-herzegovina",
},
{ code: "bw", continent: "Africa", label: "Botswana", value: "botswana" },
{ code: "br", continent: "South America", label: "Brazil", value: "brazil" },
{ code: "bn", continent: "Asia", label: "Brunei", value: "brunei" },
{ code: "bg", continent: "Europe", label: "Bulgaria", value: "bulgaria" },
{
code: "bf",
continent: "Africa",
label: "Burkina Faso",
value: "burkina-faso",
},
{ code: "bi", continent: "Africa", label: "Burundi", value: "burundi" },
{ code: "kh", continent: "Asia", label: "Cambodia", value: "cambodia" },
{ code: "cm", continent: "Africa", label: "Cameroon", value: "cameroon" },
{ code: "ca", continent: "North America", label: "Canada", value: "canada" },
{ code: "cv", continent: "Africa", label: "Cape Verde", value: "cape-verde" },
{
code: "cf",
continent: "Africa",
label: "Central African Republic",
value: "central-african-republic",
},
{ code: "td", continent: "Africa", label: "Chad", value: "chad" },
{ code: "cl", continent: "South America", label: "Chile", value: "chile" },
{ code: "cn", continent: "Asia", label: "China", value: "china" },
{
code: "co",
continent: "South America",
label: "Colombia",
value: "colombia",
},
{ code: "km", continent: "Africa", label: "Comoros", value: "comoros" },
{ code: "cg", continent: "Africa", label: "Congo", value: "congo" },
{
code: "cr",
continent: "North America",
label: "Costa Rica",
value: "costa-rica",
},
{ code: "hr", continent: "Europe", label: "Croatia", value: "croatia" },
{ code: "cu", continent: "North America", label: "Cuba", value: "cuba" },
{ code: "cy", continent: "Asia", label: "Cyprus", value: "cyprus" },
{
code: "cz",
continent: "Europe",
label: "Czech Republic",
value: "czech-republic",
},
{ code: "dk", continent: "Europe", label: "Denmark", value: "denmark" },
{ code: "dj", continent: "Africa", label: "Djibouti", value: "djibouti" },
{
code: "dm",
continent: "North America",
label: "Dominica",
value: "dominica",
},
{
code: "do",
continent: "North America",
label: "Dominican Republic",
value: "dominican-republic",
},
{
code: "ec",
continent: "South America",
label: "Ecuador",
value: "ecuador",
},
{ code: "eg", continent: "Africa", label: "Egypt", value: "egypt" },
{
code: "sv",
continent: "North America",
label: "El Salvador",
value: "el-salvador",
},
{
code: "gq",
continent: "Africa",
label: "Equatorial Guinea",
value: "equatorial-guinea",
},
{ code: "er", continent: "Africa", label: "Eritrea", value: "eritrea" },
{ code: "ee", continent: "Europe", label: "Estonia", value: "estonia" },
{ code: "et", continent: "Africa", label: "Ethiopia", value: "ethiopia" },
{ code: "fj", continent: "Oceania", label: "Fiji", value: "fiji" },
{ code: "fi", continent: "Europe", label: "Finland", value: "finland" },
{ code: "fr", continent: "Europe", label: "France", value: "france" },
{ code: "ga", continent: "Africa", label: "Gabon", value: "gabon" },
{ code: "gm", continent: "Africa", label: "Gambia", value: "gambia" },
{ code: "ge", continent: "Asia", label: "Georgia", value: "georgia" },
{ code: "de", continent: "Europe", label: "Germany", value: "germany" },
{ code: "gh", continent: "Africa", label: "Ghana", value: "ghana" },
{ code: "gr", continent: "Europe", label: "Greece", value: "greece" },
{
code: "gd",
continent: "North America",
label: "Grenada",
value: "grenada",
},
{
code: "gt",
continent: "North America",
label: "Guatemala",
value: "guatemala",
},
{ code: "gn", continent: "Africa", label: "Guinea", value: "guinea" },
{
code: "gw",
continent: "Africa",
label: "Guinea-Bissau",
value: "guinea-bissau",
},
{ code: "gy", continent: "South America", label: "Guyana", value: "guyana" },
{ code: "ht", continent: "North America", label: "Haiti", value: "haiti" },
{
code: "hn",
continent: "North America",
label: "Honduras",
value: "honduras",
},
{ code: "hu", continent: "Europe", label: "Hungary", value: "hungary" },
{ code: "is", continent: "Europe", label: "Iceland", value: "iceland" },
{ code: "in", continent: "Asia", label: "India", value: "india" },
{ code: "id", continent: "Asia", label: "Indonesia", value: "indonesia" },
{ code: "ir", continent: "Asia", label: "Iran", value: "iran" },
{ code: "iq", continent: "Asia", label: "Iraq", value: "iraq" },
{ code: "ie", continent: "Europe", label: "Ireland", value: "ireland" },
{ code: "il", continent: "Asia", label: "Israel", value: "israel" },
{ code: "it", continent: "Europe", label: "Italy", value: "italy" },
{
code: "jm",
continent: "North America",
label: "Jamaica",
value: "jamaica",
},
{ code: "jp", continent: "Asia", label: "Japan", value: "japan" },
{ code: "jo", continent: "Asia", label: "Jordan", value: "jordan" },
{ code: "kz", continent: "Asia", label: "Kazakhstan", value: "kazakhstan" },
{ code: "ke", continent: "Africa", label: "Kenya", value: "kenya" },
{ code: "kw", continent: "Asia", label: "Kuwait", value: "kuwait" },
{ code: "kg", continent: "Asia", label: "Kyrgyzstan", value: "kyrgyzstan" },
{ code: "la", continent: "Asia", label: "Laos", value: "laos" },
{ code: "lv", continent: "Europe", label: "Latvia", value: "latvia" },
{ code: "lb", continent: "Asia", label: "Lebanon", value: "lebanon" },
{ code: "ls", continent: "Africa", label: "Lesotho", value: "lesotho" },
{ code: "lr", continent: "Africa", label: "Liberia", value: "liberia" },
{ code: "ly", continent: "Africa", label: "Libya", value: "libya" },
{
code: "li",
continent: "Europe",
label: "Liechtenstein",
value: "liechtenstein",
},
{ code: "lt", continent: "Europe", label: "Lithuania", value: "lithuania" },
{ code: "lu", continent: "Europe", label: "Luxembourg", value: "luxembourg" },
{ code: "mg", continent: "Africa", label: "Madagascar", value: "madagascar" },
{ code: "mw", continent: "Africa", label: "Malawi", value: "malawi" },
{ code: "my", continent: "Asia", label: "Malaysia", value: "malaysia" },
{ code: "mv", continent: "Asia", label: "Maldives", value: "maldives" },
{ code: "ml", continent: "Africa", label: "Mali", value: "mali" },
{ code: "mt", continent: "Europe", label: "Malta", value: "malta" },
{
code: "mh",
continent: "Oceania",
label: "Marshall Islands",
value: "marshall-islands",
},
{ code: "mr", continent: "Africa", label: "Mauritania", value: "mauritania" },
{ code: "mu", continent: "Africa", label: "Mauritius", value: "mauritius" },
{ code: "mx", continent: "North America", label: "Mexico", value: "mexico" },
{
code: "fm",
continent: "Oceania",
label: "Micronesia",
value: "micronesia",
},
{ code: "md", continent: "Europe", label: "Moldova", value: "moldova" },
{ code: "mc", continent: "Europe", label: "Monaco", value: "monaco" },
{ code: "mn", continent: "Asia", label: "Mongolia", value: "mongolia" },
{ code: "me", continent: "Europe", label: "Montenegro", value: "montenegro" },
{ code: "ma", continent: "Africa", label: "Morocco", value: "morocco" },
{ code: "mz", continent: "Africa", label: "Mozambique", value: "mozambique" },
{ code: "mm", continent: "Asia", label: "Myanmar", value: "myanmar" },
{ code: "na", continent: "Africa", label: "Namibia", value: "namibia" },
{ code: "nr", continent: "Oceania", label: "Nauru", value: "nauru" },
{ code: "np", continent: "Asia", label: "Nepal", value: "nepal" },
{
code: "nl",
continent: "Europe",
label: "Netherlands",
value: "netherlands",
},
{
code: "nz",
continent: "Oceania",
label: "New Zealand",
value: "new-zealand",
},
{
code: "ni",
continent: "North America",
label: "Nicaragua",
value: "nicaragua",
},
{ code: "ne", continent: "Africa", label: "Niger", value: "niger" },
{ code: "ng", continent: "Africa", label: "Nigeria", value: "nigeria" },
{ code: "kp", continent: "Asia", label: "North Korea", value: "north-korea" },
{
code: "mk",
continent: "Europe",
label: "North Macedonia",
value: "north-macedonia",
},
{ code: "no", continent: "Europe", label: "Norway", value: "norway" },
{ code: "om", continent: "Asia", label: "Oman", value: "oman" },
{ code: "pk", continent: "Asia", label: "Pakistan", value: "pakistan" },
{ code: "pw", continent: "Oceania", label: "Palau", value: "palau" },
{ code: "ps", continent: "Asia", label: "Palestine", value: "palestine" },
{ code: "pa", continent: "North America", label: "Panama", value: "panama" },
{
code: "pg",
continent: "Oceania",
label: "Papua New Guinea",
value: "papua-new-guinea",
},
{
code: "py",
continent: "South America",
label: "Paraguay",
value: "paraguay",
},
{ code: "pe", continent: "South America", label: "Peru", value: "peru" },
{ code: "ph", continent: "Asia", label: "Philippines", value: "philippines" },
{ code: "pl", continent: "Europe", label: "Poland", value: "poland" },
{ code: "pt", continent: "Europe", label: "Portugal", value: "portugal" },
{ code: "qa", continent: "Asia", label: "Qatar", value: "qatar" },
{ code: "ro", continent: "Europe", label: "Romania", value: "romania" },
{ code: "ru", continent: "Europe", label: "Russia", value: "russia" },
{ code: "rw", continent: "Africa", label: "Rwanda", value: "rwanda" },
{ code: "ws", continent: "Oceania", label: "Samoa", value: "samoa" },
{ code: "sm", continent: "Europe", label: "San Marino", value: "san-marino" },
{
code: "sa",
continent: "Asia",
label: "Saudi Arabia",
value: "saudi-arabia",
},
{ code: "sn", continent: "Africa", label: "Senegal", value: "senegal" },
{ code: "rs", continent: "Europe", label: "Serbia", value: "serbia" },
{ code: "sc", continent: "Africa", label: "Seychelles", value: "seychelles" },
{
code: "sl",
continent: "Africa",
label: "Sierra Leone",
value: "sierra-leone",
},
{ code: "sg", continent: "Asia", label: "Singapore", value: "singapore" },
{ code: "sk", continent: "Europe", label: "Slovakia", value: "slovakia" },
{ code: "si", continent: "Europe", label: "Slovenia", value: "slovenia" },
{
code: "sb",
continent: "Oceania",
label: "Solomon Islands",
value: "solomon-islands",
},
{ code: "so", continent: "Africa", label: "Somalia", value: "somalia" },
{
code: "za",
continent: "Africa",
label: "South Africa",
value: "south-africa",
},
{ code: "kr", continent: "Asia", label: "South Korea", value: "south-korea" },
{
code: "ss",
continent: "Africa",
label: "South Sudan",
value: "south-sudan",
},
{ code: "es", continent: "Europe", label: "Spain", value: "spain" },
{ code: "lk", continent: "Asia", label: "Sri Lanka", value: "sri-lanka" },
{ code: "sd", continent: "Africa", label: "Sudan", value: "sudan" },
{
code: "sr",
continent: "South America",
label: "Suriname",
value: "suriname",
},
{ code: "se", continent: "Europe", label: "Sweden", value: "sweden" },
{
code: "ch",
continent: "Europe",
label: "Switzerland",
value: "switzerland",
},
{ code: "sy", continent: "Asia", label: "Syria", value: "syria" },
{ code: "tw", continent: "Asia", label: "Taiwan", value: "taiwan" },
{ code: "tj", continent: "Asia", label: "Tajikistan", value: "tajikistan" },
{ code: "tz", continent: "Africa", label: "Tanzania", value: "tanzania" },
{ code: "th", continent: "Asia", label: "Thailand", value: "thailand" },
{ code: "tl", continent: "Asia", label: "Timor-Leste", value: "timor-leste" },
{ code: "tg", continent: "Africa", label: "Togo", value: "togo" },
{ code: "to", continent: "Oceania", label: "Tonga", value: "tonga" },
{
code: "tt",
continent: "North America",
label: "Trinidad and Tobago",
value: "trinidad-and-tobago",
},
{ code: "tn", continent: "Africa", label: "Tunisia", value: "tunisia" },
{ code: "tr", continent: "Asia", label: "Turkey", value: "turkey" },
{
code: "tm",
continent: "Asia",
label: "Turkmenistan",
value: "turkmenistan",
},
{ code: "tv", continent: "Oceania", label: "Tuvalu", value: "tuvalu" },
{ code: "ug", continent: "Africa", label: "Uganda", value: "uganda" },
{ code: "ua", continent: "Europe", label: "Ukraine", value: "ukraine" },
{
code: "ae",
continent: "Asia",
label: "United Arab Emirates",
value: "united-arab-emirates",
},
{
code: "gb",
continent: "Europe",
label: "United Kingdom",
value: "united-kingdom",
},
{
code: "us",
continent: "North America",
label: "United States",
value: "united-states",
},
{
code: "uy",
continent: "South America",
label: "Uruguay",
value: "uruguay",
},
{ code: "uz", continent: "Asia", label: "Uzbekistan", value: "uzbekistan" },
{ code: "vu", continent: "Oceania", label: "Vanuatu", value: "vanuatu" },
{
code: "va",
continent: "Europe",
label: "Vatican City",
value: "vatican-city",
},
{
code: "ve",
continent: "South America",
label: "Venezuela",
value: "venezuela",
},
{ code: "vn", continent: "Asia", label: "Vietnam", value: "vietnam" },
{ code: "ye", continent: "Asia", label: "Yemen", value: "yemen" },
{ code: "zm", continent: "Africa", label: "Zambia", value: "zambia" },
{ code: "zw", continent: "Africa", label: "Zimbabwe", value: "zimbabwe" },
];
export default function Particle() {
return (
<Combobox defaultValue={countries[0]} items={countries}>
<ComboboxTrigger
render={
<Button
className="w-full justify-between font-normal"
variant="outline"
/>
}
>
<ComboboxValue />
<ChevronsUpDownIcon className="-me-1!" />
</ComboboxTrigger>
<ComboboxPopup aria-label="Select country">
<div className="border-b p-2">
<ComboboxInput
className="rounded-md before:rounded-[calc(var(--radius-md)-1px)]"
placeholder="e.g. United Kingdom"
showTrigger={false}
startAddon={<SearchIcon />}
/>
</div>
<ComboboxEmpty>No countries found.</ComboboxEmpty>
<ComboboxList>
{(country: Country) => (
<ComboboxItem key={country.code} value={country}>
{country.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
With Select Button
Use SelectButton as the trigger render target to match the visual style of a native select field with an inline popup search.
"use client";
import { SearchIcon } from "lucide-react";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
import { SelectButton } from "@/components/ui/select";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
return (
<Combobox items={items}>
<ComboboxTrigger render={<SelectButton />}>
<ComboboxValue placeholder="Select a fruit" />
</ComboboxTrigger>
<ComboboxPopup aria-label="Select a fruit">
<div className="border-b p-2">
<ComboboxInput
className="rounded-md before:rounded-[calc(var(--radius-md)-1px)]"
placeholder="Search fruits..."
showTrigger={false}
startAddon={<SearchIcon />}
/>
</div>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Form Integration
Place a combobox inside Form and Field to wire up label association, required validation, and a visible FieldError.
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const selectedItem = formData.get("item");
const itemValue =
items.find((item) => item.label === selectedItem)?.value || selectedItem;
setLoading(true);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
alert(`Favorite item: ${itemValue || ""}`);
};
return (
<Form className="flex w-full max-w-64 flex-col gap-4" onSubmit={onSubmit}>
<Field name="item">
<FieldLabel>Favorite item</FieldLabel>
<Combobox items={items} required>
<ComboboxInput placeholder="Select an item..." />
<ComboboxPopup>
<ComboboxEmpty>No results found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<FieldError>Please select a item.</FieldError>
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
);
}
Form Integration - Multiple
Combine multiple selection with form validation to require at least one chip before the form can be submitted.
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxChip,
ComboboxChips,
ComboboxChipsInput,
ComboboxEmpty,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxValue,
} from "@/components/ui/combobox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
const items = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Orange", value: "orange" },
{ label: "Grape", value: "grape" },
{ label: "Strawberry", value: "strawberry" },
{ label: "Mango", value: "mango" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Kiwi", value: "kiwi" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
];
export default function Particle() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const selectedItems = formData.getAll("items");
const itemValues = selectedItems.map(
(selectedItem) =>
items.find((item) => item.label === selectedItem)?.value ||
selectedItem,
);
setLoading(true);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
alert(`Favorite items: ${itemValues.join(", ") || ""}`);
};
return (
<Form className="flex w-full max-w-64 flex-col gap-4" onSubmit={onSubmit}>
<Field name="items">
<FieldLabel>Favorite items</FieldLabel>
<Combobox items={items} multiple required>
<ComboboxChips>
<ComboboxValue>
{(value: { value: string; label: string }[]) => (
<>
{value?.map((item) => (
<ComboboxChip aria-label={item.label} key={item.value}>
{item.label}
</ComboboxChip>
))}
<ComboboxChipsInput
placeholder={value.length > 0 ? undefined : "Select items…"}
/>
</>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxPopup>
<ComboboxEmpty>No items found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<FieldError>Please select at least one item.</FieldError>
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
);
}
Async Search
Debounce user input and fetch results on demand. A spinner replaces the search icon while the request is in flight.
"use client";
import { Loader2Icon, SearchIcon } from "lucide-react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
interface Repo {
label: string;
value: string;
language: string;
stars: string;
}
const allRepos: Repo[] = [
{
label: "facebook/react",
language: "JavaScript",
stars: "228k",
value: "react",
},
{
label: "vercel/next.js",
language: "TypeScript",
stars: "126k",
value: "nextjs",
},
{
label: "tailwindlabs/tailwindcss",
language: "CSS",
stars: "83k",
value: "tailwindcss",
},
{ label: "vitejs/vite", language: "TypeScript", stars: "68k", value: "vite" },
{ label: "vuejs/vue", language: "TypeScript", stars: "207k", value: "vue" },
{
label: "sveltejs/svelte",
language: "TypeScript",
stars: "79k",
value: "svelte",
},
{
label: "microsoft/typescript",
language: "TypeScript",
stars: "100k",
value: "typescript",
},
{
label: "prisma/prisma",
language: "TypeScript",
stars: "39k",
value: "prisma",
},
{ label: "trpc/trpc", language: "TypeScript", stars: "35k", value: "trpc" },
{
label: "radix-ui/primitives",
language: "TypeScript",
stars: "16k",
value: "radix-ui",
},
{
label: "pmndrs/zustand",
language: "TypeScript",
stars: "48k",
value: "zustand",
},
{
label: "tanstack/query",
language: "TypeScript",
stars: "42k",
value: "tanstack-query",
},
];
async function searchRepos(query: string): Promise<Repo[]> {
await new Promise((r) => setTimeout(r, 400));
if (!query) return allRepos.slice(0, 5);
return allRepos.filter((r) =>
r.label.toLowerCase().includes(query.toLowerCase()),
);
}
export default function Particle() {
const [inputValue, setInputValue] = useState("");
const [results, setResults] = useState<Repo[]>(allRepos.slice(0, 5));
const [loading, setLoading] = useState(false);
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const handleInputChange = useCallback((value: string) => {
setInputValue(value);
if (debounceRef.current) clearTimeout(debounceRef.current);
setLoading(true);
debounceRef.current = setTimeout(async () => {
const data = await searchRepos(value);
setResults(data);
setLoading(false);
}, 300);
}, []);
useEffect(() => {
return () => {
if (debounceRef.current) clearTimeout(debounceRef.current);
};
}, []);
return (
<Combobox items={results}>
<ComboboxInput
aria-label="Search repositories"
onChange={(e) => handleInputChange(e.target.value)}
placeholder="Search repositories..."
startAddon={
loading ? <Loader2Icon className="animate-spin" /> : <SearchIcon />
}
value={inputValue}
/>
<ComboboxPopup>
<ComboboxEmpty>
{loading ? "Searching..." : "No repositories found."}
</ComboboxEmpty>
<ComboboxList>
{(repo: Repo) => (
<ComboboxItem key={repo.value} value={repo}>
<div className="flex w-full items-center justify-between gap-4">
<span>{repo.label}</span>
<div className="flex shrink-0 items-center gap-2 text-muted-foreground text-xs">
<span>{repo.language}</span>
<span>★ {repo.stars}</span>
</div>
</div>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Rich Item Rendering
Render custom JSX inside each ComboboxItem — here a coloured avatar, a name, and a role subtitle — for an assignee-style picker.
"use client";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
interface TeamMember {
label: string;
value: string;
role: string;
initials: string;
color: string;
}
const members: TeamMember[] = [
{
color: "bg-violet-500",
initials: "AJ",
label: "Alice Johnson",
role: "Engineering Lead",
value: "alice",
},
{
color: "bg-blue-500",
initials: "BM",
label: "Bob Martinez",
role: "Senior Frontend",
value: "bob",
},
{
color: "bg-pink-500",
initials: "CW",
label: "Carol White",
role: "Product Designer",
value: "carol",
},
{
color: "bg-amber-500",
initials: "DK",
label: "David Kim",
role: "Backend Engineer",
value: "david",
},
{
color: "bg-emerald-500",
initials: "EC",
label: "Eva Chen",
role: "DevOps Engineer",
value: "eva",
},
{
color: "bg-red-500",
initials: "FL",
label: "Frank Lee",
role: "QA Engineer",
value: "frank",
},
{
color: "bg-cyan-500",
initials: "GP",
label: "Grace Park",
role: "Data Scientist",
value: "grace",
},
{
color: "bg-orange-500",
initials: "HB",
label: "Henry Brown",
role: "Security Engineer",
value: "henry",
},
];
export default function Particle() {
return (
<Combobox items={members}>
<ComboboxInput aria-label="Assign to" placeholder="Assign to..." />
<ComboboxPopup>
<ComboboxEmpty>No team members found.</ComboboxEmpty>
<ComboboxList>
{(member: TeamMember) => (
<ComboboxItem
className="**:data-check:ms-auto"
key={member.value}
value={member}
>
<div className="flex items-center gap-3">
<span
className={`flex size-8 shrink-0 items-center justify-center rounded-full font-semibold text-white text-xs ${member.color}`}
>
{member.initials}
</span>
<div className="flex flex-col">
<span className="font-medium text-sm">{member.label}</span>
<span className="text-muted-foreground text-xs">
{member.role}
</span>
</div>
</div>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Controlled
Manage the selected value with external state via value and onValueChange, and drive a programmatic clear from a sibling button.
"use client";
import { ChevronsUpDownIcon, SearchIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
interface Status {
label: string;
value: string;
color: string;
}
const statuses: Status[] = [
{ color: "bg-slate-400", label: "Backlog", value: "backlog" },
{ color: "bg-blue-400", label: "Todo", value: "todo" },
{ color: "bg-amber-400", label: "In Progress", value: "in-progress" },
{ color: "bg-violet-400", label: "In Review", value: "in-review" },
{ color: "bg-emerald-400", label: "Done", value: "done" },
{ color: "bg-red-400", label: "Cancelled", value: "cancelled" },
];
export default function Particle() {
const [value, setValue] = useState<Status | null>(statuses[0] ?? null);
return (
<div className="flex w-full max-w-sm flex-col gap-4">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-sm">Current status:</span>
{value ? (
<Badge className="gap-1.5" variant="outline">
<span className={`size-2 rounded-full ${value.color}`} />
{value.label}
</Badge>
) : (
<span className="text-muted-foreground text-sm">None</span>
)}
</div>
<div className="flex gap-2">
<Combobox
items={statuses}
onValueChange={(v) => setValue(v as Status | null)}
value={value}
>
<ComboboxTrigger
render={
<Button
className="flex-1 justify-between font-normal"
variant="outline"
/>
}
>
{value ? (
<span className="flex items-center gap-2">
<span className={`size-2 rounded-full ${value.color}`} />
<ComboboxValue />
</span>
) : (
<ComboboxValue placeholder="Set status..." />
)}
<ChevronsUpDownIcon className="-me-1!" />
</ComboboxTrigger>
<ComboboxPopup aria-label="Set status">
<div className="border-b p-2">
<ComboboxInput
className="rounded-md before:rounded-[calc(var(--radius-md)-1px)]"
placeholder="Search status..."
showTrigger={false}
startAddon={<SearchIcon />}
/>
</div>
<ComboboxEmpty>No status found.</ComboboxEmpty>
<ComboboxList>
{(status: Status) => (
<ComboboxItem key={status.value} value={status}>
<span className={`size-2 rounded-full ${status.color}`} />
{status.label}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<Button
aria-label="Clear status"
disabled={!value}
onClick={() => setValue(null)}
size="icon"
variant="ghost"
>
<XIcon />
</Button>
</div>
</div>
);
}
City Picker
A searchable city picker with timezone and country code shown as trailing metadata in each dropdown item, and the selected city's timezone displayed below.
"use client";
import { MapPinIcon } from "lucide-react";
import { useState } from "react";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
} from "@/components/ui/combobox";
interface City {
label: string;
value: string;
country: string;
timezone: string;
}
const cities: City[] = [
{ country: "US", label: "New York", timezone: "UTC-5", value: "nyc" },
{ country: "UK", label: "London", timezone: "UTC+0", value: "lon" },
{ country: "JP", label: "Tokyo", timezone: "UTC+9", value: "tyo" },
{ country: "FR", label: "Paris", timezone: "UTC+1", value: "par" },
{ country: "AU", label: "Sydney", timezone: "UTC+11", value: "syd" },
{ country: "SG", label: "Singapore", timezone: "UTC+8", value: "sin" },
{ country: "DE", label: "Berlin", timezone: "UTC+1", value: "ber" },
{ country: "IN", label: "Mumbai", timezone: "UTC+5:30", value: "bom" },
{ country: "CA", label: "Toronto", timezone: "UTC-5", value: "yyz" },
{ country: "BR", label: "São Paulo", timezone: "UTC-3", value: "gru" },
];
export default function Particle() {
const [selected, setSelected] = useState<City | null>(null);
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<Combobox
items={cities}
onValueChange={(v) => setSelected(v as City | null)}
value={selected}
>
<ComboboxInput
placeholder="Search city..."
startAddon={<MapPinIcon />}
/>
<ComboboxPopup>
<ComboboxEmpty>No cities found.</ComboboxEmpty>
<ComboboxList>
{(city: City) => (
<ComboboxItem
className="**:data-check:ms-auto"
key={city.value}
value={city}
>
<div className="flex flex-1 items-center justify-between">
<span className="text-sm">{city.label}</span>
<span className="text-muted-foreground text-xs">
{city.country} · {city.timezone}
</span>
</div>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
{selected && (
<div className="rounded-lg border px-4 py-2.5 text-sm">
<span className="text-muted-foreground">Selected: </span>
<span className="font-medium">{selected.label}</span>
<span className="ml-2 text-muted-foreground text-xs">
{selected.timezone}
</span>
</div>
)}
</div>
);
}
Sprint Selector
A trigger-button combobox listing sprints with status badges (active, upcoming, completed) and date ranges. The selected sprint updates a preview badge in the trigger.
"use client";
import { ChevronsUpDownIcon } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
interface Sprint {
label: string;
value: string;
status: "active" | "upcoming" | "completed";
dates: string;
}
const sprints: Sprint[] = [
{ dates: "Jun 2–15", label: "Sprint 24", status: "active", value: "s24" },
{ dates: "Jun 16–29", label: "Sprint 25", status: "upcoming", value: "s25" },
{
dates: "Jun 30–Jul 13",
label: "Sprint 26",
status: "upcoming",
value: "s26",
},
{
dates: "May 19–Jun 1",
label: "Sprint 23",
status: "completed",
value: "s23",
},
{ dates: "May 5–18", label: "Sprint 22", status: "completed", value: "s22" },
];
const statusVariant: Record<
Sprint["status"],
"success" | "info" | "secondary"
> = {
active: "success",
completed: "secondary",
upcoming: "info",
};
export default function Particle() {
const [value, setValue] = useState<Sprint | null>(sprints[0] ?? null);
return (
<Combobox
items={sprints}
onValueChange={(v) => setValue(v as Sprint | null)}
value={value}
>
<ComboboxTrigger
render={
<Button
className="w-full max-w-xs justify-between font-normal"
variant="outline"
/>
}
>
<ComboboxValue placeholder="Select sprint..." />
<ChevronsUpDownIcon className="-me-1!" />
</ComboboxTrigger>
<ComboboxPopup className="w-64">
<ComboboxInput placeholder="Search sprints..." showTrigger={false} />
<ComboboxEmpty>No sprints found.</ComboboxEmpty>
<ComboboxList>
{(sprint: Sprint) => (
<ComboboxItem
className="**:data-check:ms-auto"
key={sprint.value}
value={sprint}
>
<div className="flex flex-1 items-center justify-between gap-2">
<span className="text-sm">{sprint.label}</span>
<div className="flex items-center gap-2">
<span className="text-muted-foreground text-xs">
{sprint.dates}
</span>
<Badge size="sm" variant={statusVariant[sprint.status]}>
{sprint.status}
</Badge>
</div>
</div>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
);
}
Font Picker
Items are grouped by category (Sans-serif, Serif, Monospace) with ComboboxGroup and ComboboxSeparator. Selecting a font renders a live preview sentence below in that typeface.
The quick brown fox jumps over the lazy dog.
"use client";
import { ChevronsUpDownIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxEmpty,
ComboboxGroup,
ComboboxGroupLabel,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxSeparator,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
interface Font {
label: string;
value: string;
category: "sans" | "serif" | "mono";
}
const fonts: Font[] = [
{ category: "sans", label: "Inter", value: "inter" },
{ category: "sans", label: "Geist", value: "geist" },
{ category: "sans", label: "DM Sans", value: "dm-sans" },
{ category: "serif", label: "Lora", value: "lora" },
{ category: "serif", label: "Playfair Display", value: "playfair" },
{ category: "mono", label: "Geist Mono", value: "geist-mono" },
{ category: "mono", label: "JetBrains Mono", value: "jetbrains" },
{ category: "mono", label: "Fira Code", value: "fira-code" },
];
const groups = [
{
id: "sans",
items: fonts.filter((f) => f.category === "sans"),
label: "Sans-serif",
},
{
id: "serif",
items: fonts.filter((f) => f.category === "serif"),
label: "Serif",
},
{
id: "mono",
items: fonts.filter((f) => f.category === "mono"),
label: "Monospace",
},
];
export default function Particle() {
const [value, setValue] = useState<Font | null>(fonts[0] ?? null);
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<Combobox
items={fonts}
onValueChange={(v) => setValue(v as Font | null)}
value={value}
>
<ComboboxTrigger
render={
<Button
className="w-full justify-between font-normal"
variant="outline"
/>
}
>
<ComboboxValue placeholder="Select font..." />
<ChevronsUpDownIcon className="-me-1!" />
</ComboboxTrigger>
<ComboboxPopup>
<ComboboxInput placeholder="Search fonts..." showTrigger={false} />
<ComboboxEmpty>No fonts found.</ComboboxEmpty>
<ComboboxList>
{groups.map((group, i) => (
<ComboboxGroup key={group.id}>
{i > 0 && <ComboboxSeparator />}
<ComboboxGroupLabel>{group.label}</ComboboxGroupLabel>
{group.items.map((font) => (
<ComboboxItem key={font.value} value={font}>
<span
className={
font.category === "mono"
? "font-mono text-sm"
: "text-sm"
}
>
{font.label}
</span>
</ComboboxItem>
))}
</ComboboxGroup>
))}
</ComboboxList>
</ComboboxPopup>
</Combobox>
{value && (
<p
className={`text-muted-foreground text-sm ${value.category === "mono" ? "font-mono" : ""}`}
>
The quick brown fox jumps over the lazy dog.
</p>
)}
</div>
);
}
Timezone Picker
Displays the UTC offset as a badge inside the trigger and shows region and identifier details below the combobox on selection. A ghost clear button sits beside the trigger.
Region: Europe · Identifier: UTC
"use client";
import { ChevronsUpDownIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
interface Timezone {
label: string;
value: string;
offset: string;
region: string;
}
const timezones: Timezone[] = [
{
label: "Pacific Time",
offset: "UTC-8",
region: "Americas",
value: "America/Los_Angeles",
},
{
label: "Mountain Time",
offset: "UTC-7",
region: "Americas",
value: "America/Denver",
},
{
label: "Central Time",
offset: "UTC-6",
region: "Americas",
value: "America/Chicago",
},
{
label: "Eastern Time",
offset: "UTC-5",
region: "Americas",
value: "America/New_York",
},
{ label: "UTC", offset: "UTC+0", region: "Europe", value: "UTC" },
{
label: "Central European",
offset: "UTC+1",
region: "Europe",
value: "Europe/Paris",
},
{
label: "Eastern European",
offset: "UTC+2",
region: "Europe",
value: "Europe/Helsinki",
},
{
label: "India Standard",
offset: "UTC+5:30",
region: "Asia",
value: "Asia/Kolkata",
},
{
label: "China Standard",
offset: "UTC+8",
region: "Asia",
value: "Asia/Shanghai",
},
{
label: "Japan Standard",
offset: "UTC+9",
region: "Asia",
value: "Asia/Tokyo",
},
{
label: "AEST",
offset: "UTC+10",
region: "Pacific",
value: "Australia/Sydney",
},
];
export default function Particle() {
const [value, setValue] = useState<Timezone | null>(
timezones.find((t) => t.value === "UTC") ?? null,
);
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<div className="flex gap-2">
<Combobox
items={timezones}
onValueChange={(v) => setValue(v as Timezone | null)}
value={value}
>
<ComboboxTrigger
render={
<Button
className="flex-1 justify-between font-normal"
variant="outline"
/>
}
>
{value ? (
<span className="flex items-center gap-2 text-sm">
<Badge size="sm" variant="secondary">
{value.offset}
</Badge>
<ComboboxValue />
</span>
) : (
<ComboboxValue placeholder="Select timezone..." />
)}
<ChevronsUpDownIcon className="-me-1!" />
</ComboboxTrigger>
<ComboboxPopup>
<ComboboxInput
placeholder="Search timezone..."
showTrigger={false}
/>
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
<ComboboxList>
{(tz: Timezone) => (
<ComboboxItem
className="**:data-check:ms-auto"
key={tz.value}
value={tz}
>
<div className="flex flex-1 items-center justify-between gap-2">
<span className="text-sm">{tz.label}</span>
<span className="text-muted-foreground text-xs">
{tz.offset}
</span>
</div>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
<Button
aria-label="Clear"
disabled={!value}
onClick={() => setValue(null)}
size="icon"
variant="ghost"
>
<XIcon />
</Button>
</div>
{value && (
<p className="text-muted-foreground text-xs">
Region:{" "}
<span className="font-medium text-foreground">{value.region}</span> ·
Identifier:{" "}
<span className="font-mono text-foreground">{value.value}</span>
</p>
)}
</div>
);
}
Currency Picker
Each option shows a symbol swatch, full currency name, and ISO code. The trigger reflects the chosen currency's symbol swatch and name, with a label below confirming the active currency.
"use client";
import { ChevronsUpDownIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Combobox,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
ComboboxPopup,
ComboboxTrigger,
ComboboxValue,
} from "@/components/ui/combobox";
interface Currency {
code: string;
label: string;
symbol: string;
value: string;
}
const currencies: Currency[] = [
{ code: "USD", label: "US Dollar", symbol: "$", value: "usd" },
{ code: "EUR", label: "Euro", symbol: "€", value: "eur" },
{ code: "GBP", label: "British Pound", symbol: "£", value: "gbp" },
{ code: "JPY", label: "Japanese Yen", symbol: "¥", value: "jpy" },
{ code: "CAD", label: "Canadian Dollar", symbol: "$", value: "cad" },
{ code: "AUD", label: "Australian Dollar", symbol: "$", value: "aud" },
{ code: "CHF", label: "Swiss Franc", symbol: "Fr", value: "chf" },
{ code: "INR", label: "Indian Rupee", symbol: "₹", value: "inr" },
{ code: "SGD", label: "Singapore Dollar", symbol: "$", value: "sgd" },
{ code: "BRL", label: "Brazilian Real", symbol: "R$", value: "brl" },
];
export default function Particle() {
const [value, setValue] = useState<Currency | null>(currencies[0] ?? null);
return (
<div className="flex w-full max-w-xs flex-col gap-3">
<Combobox
items={currencies}
onValueChange={(v) => setValue(v as Currency | null)}
value={value}
>
<ComboboxTrigger
render={
<Button
className="w-full justify-between font-normal"
variant="outline"
/>
}
>
{value ? (
<span className="flex items-center gap-2 text-sm">
<span className="flex size-6 items-center justify-center rounded bg-muted font-mono font-semibold text-xs">
{value.symbol}
</span>
<ComboboxValue />
</span>
) : (
<ComboboxValue placeholder="Select currency..." />
)}
<ChevronsUpDownIcon className="-me-1!" />
</ComboboxTrigger>
<ComboboxPopup>
<ComboboxInput placeholder="Search currency..." showTrigger={false} />
<ComboboxEmpty>No currencies found.</ComboboxEmpty>
<ComboboxList>
{(cur: Currency) => (
<ComboboxItem
className="**:data-check:ms-auto"
key={cur.value}
value={cur}
>
<span className="flex size-6 shrink-0 items-center justify-center rounded bg-muted font-mono font-semibold text-xs">
{cur.symbol}
</span>
<div className="flex flex-1 items-center justify-between">
<span className="text-sm">{cur.label}</span>
<span className="text-muted-foreground text-xs">
{cur.code}
</span>
</div>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxPopup>
</Combobox>
{value && (
<div className="rounded-lg border px-4 py-2 text-sm">
<span className="text-muted-foreground">Displaying prices in </span>
<span className="font-medium">{value.label}</span>
<span className="text-muted-foreground"> ({value.code})</span>
</div>
)}
</div>
);
}
On This Page

