Combobox

A searchable combobox with multi-select, async option loading, and creatable entries for forms, filters, and pickers.

Loading preview

Examples

Practical examples and common states for the same installable component.

Basic

Single-select combobox with client-side filtering.

examples/combobox-basic.tsx
"use client";
import * as React from "react";
import { Combobox } from "@/components/wensity/combobox";
const frameworkOptions = [
{ value: "nextjs", label: "Next.js" },
{ value: "remix", label: "Remix" },
{ value: "nuxt", label: "Nuxt" },
{ value: "sveltekit", label: "SvelteKit" },
{ value: "astro", label: "Astro" },
];
export function ComboboxBasic() {
const [value, setValue] = React.useState("nextjs");
return (
<Combobox
label="Framework"
placeholder="Search frameworks…"
value={value}
onValueChange={(next) =>
setValue(Array.isArray(next) ? (next[0] ?? "") : next)
}
options={frameworkOptions}
/>
);
}

Disabled

Combobox in a locked state - no input, no popup.

examples/combobox-disabled.tsx
import { Combobox } from "@/components/wensity/combobox";
export function ComboboxDisabled() {
return (
<Combobox
disabled
label="Region"
placeholder="Locked to primary region"
defaultValue="iad"
options={[
{ value: "iad", label: "US East" },
{ value: "fra", label: "Europe" },
]}
hint="Contact support to change region assignments."
/>
);
}

Error

Surfaces an inline validation message when required.

examples/combobox-error.tsx
"use client";
import * as React from "react";
import { Combobox } from "@/components/wensity/combobox";
const frameworkOptions = [
{ value: "nextjs", label: "Next.js" },
{ value: "remix", label: "Remix" },
{ value: "astro", label: "Astro" },
];
export function ComboboxError() {
const [value, setValue] = React.useState("");
const hasError = value.length === 0;
return (
<Combobox
required
label="Framework"
placeholder="Pick a framework"
value={value}
onValueChange={(next) =>
setValue(Array.isArray(next) ? (next[0] ?? "") : next)
}
options={frameworkOptions}
error={hasError ? "Choose a framework before continuing." : undefined}
/>
);
}

Async

Load or filter options from an external source.

examples/combobox-async.tsx
"use client";
import * as React from "react";
import { Combobox } from "@/components/wensity/combobox";
const frameworkOptions = [
{ value: "nextjs", label: "Next.js" },
{ value: "remix", label: "Remix" },
{ value: "astro", label: "Astro" },
{ value: "sveltekit", label: "SvelteKit" },
];
export function ComboboxAsync() {
const [query, setQuery] = React.useState("");
const [loading, setLoading] = React.useState(false);
const [options, setOptions] = React.useState(frameworkOptions);
const handleSearchChange = React.useCallback(
(nextQuery: string) => {
setQuery(nextQuery);
if (nextQuery !== query) setLoading(true);
},
[query],
);
React.useEffect(() => {
const timeout = window.setTimeout(() => {
setOptions(
frameworkOptions.filter((option) =>
option.label.toLowerCase().includes(query.toLowerCase()),
),
);
setLoading(false);
}, 400);
return () => window.clearTimeout(timeout);
}, [query]);
return (
<Combobox
label="Framework"
placeholder="Search frameworks…"
options={options}
loading={loading}
filter={false}
onSearchChange={handleSearchChange}
/>
);
}

Creatable

Create a new option when nothing matches the query.

examples/combobox-creatable.tsx
"use client";
import * as React from "react";
import { Combobox } from "@/components/wensity/combobox";
export function ComboboxCreatable() {
const [options, setOptions] = React.useState([
{ value: "design", label: "Design" },
{ value: "engineering", label: "Engineering" },
]);
return (
<Combobox
multiple
creatable
label="Teams"
placeholder="Search or create…"
options={options}
onCreateOption={(input) => {
const created = {
value: input.toLowerCase().replace(/\s+/g, "-"),
label: input,
};
setOptions((current) => [...current, created]);
return created;
}}
/>
);
}

Props

PropTypeDefaultDescription
optionsComboboxOption[] | ComboboxGroup[]-Choices to render. Pass groups to get labelled sections.
valuestring | string[]-Controlled selection. An array when multiple is set.
defaultValuestring | string[]-Initial selection when uncontrolled.
onValueChange(value: string | string[]) => void-Called when the selection changes.
multiplebooleanfalseAllows selecting more than one option.
creatablebooleanfalseOffers to create an option from the current query when nothing matches.
onCreateOption(inputValue: string) => ComboboxOption | void-Called when the create affordance is chosen.
loadingbooleanfalseShows a loading state in the list, for async option sources.
onSearchChange(query: string) => void-Called as the query changes. Use for server-side search.
filterbooleantrueClient-side filtering. Set false when options come from onSearchChange.
labelReact.ReactNode-Field label above the trigger.
hintReact.ReactNode-Helper copy under the trigger.
errorReact.ReactNode-Error message under the trigger, with invalid styling applied.
status"default" | "error" | "success"-Drives the success and error visuals.
placeholderstring"Search…"Query input placeholder.
emptyMessageReact.ReactNode"No results found."Shown when the query matches nothing.
closeWhenAllSelectedbooleantrueCloses the list once every option is chosen in multiple mode.
comboboxSize"sm" | "md" | "lg""md"Trigger height and text size.
fullWidthbooleantrueStretches the trigger to fill its container.