Combobox
A form-ready, searchable select component that filters a list of options as the user types. It supports both controlled and uncontrolled usage.
import { Combobox } from "@tenstorrent/vesper/combobox";
const FRUITS = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Cherry", value: "cherry" },
{ label: "Grape", value: "grape" },
{ label: "Mango", value: "mango" },
{ label: "Orange", value: "orange" },
{ label: "Peach", value: "peach" },
{ label: "Pear", value: "pear" },
{ label: "Pineapple", value: "pineapple" },
{ label: "Strawberry", value: "strawberry" },
];
export default function ComboboxDemo() {
return (
<Combobox
aria-label="Fruit"
placeholder="Select a fruit"
options={FRUITS}
/>
);
}Combobox an accessible name by either pairing it with a <label> via htmlFor or giving it an aria-label. Without one, assistive technology announces the field with no indication of what it is for.Options
| Prop | Type | Description | Default |
|---|---|---|---|
options | (ComboboxItem | string)[] | The list of selectable options displayed in the dropdown. Strings are treated as both the label and the value. | — |
size | "sm" | "md" | "lg" | The size of the combobox. Affects padding and typography. | "md" |
variant | "default" | "warning" | "success" | "error" | The visual variant which determines color scheme. | "default" |
placeholder | string | Placeholder text shown in the input when it is empty. | "Search..." |
emptyStateText | string | The text displayed in the dropdown when no options match the input's value. | "No results" |
value | string | null | The currently selected value (controlled mode). Pass null to represent no selection. | — |
defaultValue | string | null | The initial selected value (uncontrolled mode). | — |
onValueChange | (value: string | null) => void | Callback invoked with the new value when the selection changes, or null when it is cleared. | — |
inputValue | string | The current text displayed in the input (controlled mode). | — |
defaultInputValue | string | The initial text displayed in the input (uncontrolled mode). | — |
onInputValueChange | (value: string) => void | Callback invoked with the new text whenever the input's value changes. | — |
open | boolean | Controls the open state of the dropdown (controlled mode). | — |
defaultOpen | boolean | Whether the dropdown is open by default (uncontrolled mode). | false |
onOpenChange | (open: boolean) => void | Callback fired when the open state of the dropdown changes. | — |
name | string | The form field name submitted with form data. | — |
form | string | Associates the combobox with a <form> element by its id. | — |
id | string | An identifier applied to the underlying underlying <input> element. | — |
required | boolean | When true, marks the input as required for native form validation. | false |
disabled | boolean | When true, prevents interaction with the input and dropdown. | false |
readOnly | boolean | When true, the input's value cannot be edited, but it is still submitted with form data. | false |
container | HTMLElement | ShadowRoot | null | RefObject<HTMLElement | ShadowRoot | null> | Specify the element or shadow root to portal the dropdown into. | — |
ref | Ref<HTMLInputElement> | A ref forwarded to the underlying <input> element. | — |
clearButtonAriaLabel | string | Accessible label for the button that clears the currently selected value. | "Clear selection" |
dropdownTriggerAriaLabel | string | Accessible label for the button that opens the dropdown. | "Show options" |
Form props (value, disabled, name, form, etc.) are forwarded to the hidden <input> element that holds the selected combobox value. aria-* attributes, ref, input props (autoCorrect, spellCheck, autoFocus, id, etc.), and event handlers related to focus, blur, scroll, input, and change events are forwarded to the visible <input> element the user can type in to filter results. All other props are forwarded to the wrapping <div> element.
ComboboxItem options
| Property | Type | Description |
|---|---|---|
label | string | The text displayed for this option in the dropdown, and used to filter options as the user types. |
value | string | The underlying value submitted with form data and passed to onValueChange. Must be unique. |
Examples
Basic usage
Render a Combobox by supplying an array of options. Each option can either be a ComboboxItem, or a plain string. Use ComboboxItem objects when the value associated with each item differs from the text you want to display:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function ObjectOptionsCombobox() {
return (
<Combobox
aria-label="Country"
placeholder="Select your country"
options={[
{ label: "Canada", value: "ca" },
{ label: "Japan", value: "jp" },
{ label: "Norway", value: "no" },
]}
/>
);
}When the displayed text is the same as the value of the item, a string option is a convenient shorthand:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function StringOptionsCombobox() {
return (
<Combobox
aria-label="Country"
placeholder="Select your country"
options={["Canada", "Japan", "Norway"]}
/>
);
}Both shapes can be mixed in the same options array. Options are filtered against their label as the user types, and selecting an option fills the input with that option's label while reporting its value.
Uncontrolled vs controlled
Render a Combobox in an uncontrolled fashion to let it keep track of its own selection. Pass defaultValue if you need an option to be selected initially:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function UncontrolledCombobox() {
return (
<Combobox
aria-label="Country"
placeholder="Select your country"
defaultValue="Japan"
options={["Canada", "Japan", "Norway"]}
/>
);
}If you need to control which option is selected, use the value and onValueChange props. value is the value of the selected option, and is null when nothing is selected. Clearing the selection with the clear button calls onValueChange with null:
Selected value: apple
import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Combobox } from "@tenstorrent/vesper/combobox";
import { Typography } from "@tenstorrent/vesper/typography";
const FRUITS = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Cherry", value: "cherry" },
];
export default function ControlledCombobox() {
const [fruit, setFruit] = useState<string | null>("apple");
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--vesper-spacing-4)",
}}
>
<Combobox
aria-label="Fruit"
placeholder="Select a fruit"
options={FRUITS}
value={fruit}
onValueChange={setFruit}
/>
<Typography variant="copy-sm">
Selected value: {fruit ?? "null"}
</Typography>
<Button size="sm" onClick={() => setFruit(null)}>
Reset
</Button>
</div>
);
}Controlling the input's text
The text typed into the input is tracked separately from the selected value. Use defaultInputValue to seed the input's text, or inputValue and onInputValueChange to control it:
Input value: empty
import { useState } from "react";
import { Combobox } from "@tenstorrent/vesper/combobox";
import { Typography } from "@tenstorrent/vesper/typography";
export default function ControlledInputValueCombobox() {
const [query, setQuery] = useState("");
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--vesper-spacing-4)",
}}
>
<Combobox
aria-label="Country"
placeholder="Select your country"
options={["Canada", "Japan", "Norway"]}
inputValue={query}
onInputValueChange={setQuery}
/>
<Typography variant="copy-sm">Input value: {query || "empty"}</Typography>
</div>
);
}Controlling the input's text is most useful when the options themselves depend on what the user typed, eg. when they are fetched from a server:
import { useEffect, useState } from "react";
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function AsyncCombobox() {
const [query, setQuery] = useState("");
const [options, setOptions] = useState<string[]>([]);
useEffect(() => {
const controller = new AbortController();
fetch(`/api/repositories?q=${encodeURIComponent(query)}`, {
signal: controller.signal,
})
.then((response) => response.json())
.then((repositories: string[]) => setOptions(repositories))
.catch(() => {});
return () => controller.abort();
}, [query]);
return (
<Combobox
aria-label="Repositories"
placeholder="Search repositories"
options={options}
inputValue={query}
onInputValueChange={setQuery}
/>
);
}Different sizes
The Combobox component can be rendered at sm, md, or lg size, defaulting to md. Size affects the height, padding, and text styles of the input, as well as the size of its icons.
import { Combobox } from "@tenstorrent/vesper/combobox";
const COUNTRIES = ["Canada", "Japan", "Norway"];
export default function ComboboxSizes() {
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--vesper-spacing-4)",
}}
>
<Combobox
size="sm"
aria-label="Country"
placeholder="A small combobox"
options={COUNTRIES}
/>
<Combobox
size="md"
aria-label="Country"
placeholder="A medium combobox"
options={COUNTRIES}
/>
<Combobox
size="lg"
aria-label="Country"
placeholder="A large combobox"
options={COUNTRIES}
/>
</div>
);
}Variants
The Combobox component can be rendered in one of four variants: default, warning, success, or error. The variant determines the colour scheme of the input.
Default variant
A Combobox renders with the default variant when no variant prop is specified.
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function DefaultCombobox() {
return (
<Combobox
aria-label="Country"
placeholder="Select your country"
options={["Canada", "Japan", "Norway"]}
/>
);
}Warning variant
Use the warning variant to flag a selection that needs attention, but that doesn't prevent the form from being submitted:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function WarningCombobox() {
return (
<Combobox
aria-label="Region"
variant="warning"
defaultValue="ap"
options={[
{ label: "United States", value: "us" },
{ label: "Europe", value: "eu" },
{ label: "Asia Pacific", value: "ap" },
]}
/>
);
}Success variant
The success variant is best suited for confirming that a selection has been validated as expected:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function SuccessCombobox() {
return (
<Combobox
aria-label="Region"
variant="success"
defaultValue="eu"
options={[
{ label: "United States", value: "us" },
{ label: "Europe", value: "eu" },
{ label: "Asia Pacific", value: "ap" },
]}
/>
);
}Error variant
The error variant highlights a missing or invalid selection that must be corrected before the form can be submitted:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function ErrorCombobox() {
return (
<Combobox
required
aria-label="Region"
variant="error"
placeholder="Select a region"
options={[
{ label: "United States", value: "us" },
{ label: "Europe", value: "eu" },
{ label: "Asia Pacific", value: "ap" },
]}
/>
);
}Customising the empty state
When no option matches the text in the input, the dropdown renders a short empty state message. Use emptyStateText to tailor it to the data being searched:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function ComboboxWithCustomEmptyState() {
return (
<Combobox
aria-label="Country"
placeholder="Select your country"
options={["Canada", "Japan", "Norway"]}
emptyStateText="No matching countries"
/>
);
}Disabled and read-only
Pass disabled to prevent all interaction with the input and its dropdown:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function DisabledCombobox() {
return (
<Combobox
disabled
aria-label="Country"
defaultValue="Japan"
options={["Canada", "Japan", "Norway"]}
/>
);
}Pass readOnly when the value should be visible and submitted with the form, but not editable:
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function ReadOnlyCombobox() {
return (
<Combobox
readOnly
aria-label="Country"
defaultValue="Japan"
options={["Canada", "Japan", "Norway"]}
/>
);
}Controlling the dropdown
The dropdown opens when the input or the caret is clicked, and as the user types. You can open it initially with defaultOpen, or control it entirely with open and onOpenChange:
import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function ControlledDropdownCombobox() {
const [open, setOpen] = useState(false);
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "var(--vesper-spacing-4)",
}}
>
<Combobox
aria-label="Country"
placeholder="Select your country"
options={["Canada", "Japan", "Norway"]}
open={open}
onOpenChange={setOpen}
/>
<Button size="sm" onClick={() => setOpen((isOpen) => !isOpen)}>
{open ? "Close" : "Open"} dropdown
</Button>
</div>
);
}Usage in forms
Combobox renders a form control, so passing a name includes the selected value in form submissions, and required participates in native form validation:
import { useState } from "react";
import { Button } from "@tenstorrent/vesper/button";
import { Combobox } from "@tenstorrent/vesper/combobox";
import { Typography } from "@tenstorrent/vesper/typography";
const FRUITS = [
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Cherry", value: "cherry" },
];
export default function FormComboboxDemo() {
const [submitted, setSubmitted] = useState<string | null>(null);
return (
<form
style={{
display: "flex",
flexDirection: "column",
gap: "var(--vesper-spacing-4)",
}}
onSubmit={(event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
setSubmitted(String(data.get("fruit") ?? ""));
}}
>
<Combobox
required
name="fruit"
aria-label="Fruit"
placeholder="Select a fruit"
options={FRUITS}
/>
<Button size="sm" type="submit">
Submit
</Button>
{submitted !== null && (
<Typography variant="copy-sm">Submitted value: {submitted}</Typography>
)}
</form>
);
}The value submitted is the value of the selected option, not the text displayed in the input. The form above submits fruit=apple when "Apple" is selected.
Use the form prop to associate the field with a <form> rendered elsewhere on the page:
import { Button } from "@tenstorrent/vesper/button";
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function ComboboxFormPropDemo() {
return (
<div>
<form id="preferences" action="/api/preferences" method="post">
<Button type="submit">Save preferences</Button>
</form>
{/* rendered outside of the form, but submitted with it */}
<Combobox
required
form="preferences"
name="fruit"
aria-label="Fruit"
placeholder="Select a fruit"
options={[
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Cherry", value: "cherry" },
]}
/>
</div>
);
}Accessing the underlying element
Use the ref prop when you need direct access to the underlying <input> element, eg. to focus it when a page loads:
import { useEffect, useRef } from "react";
import { Combobox } from "@tenstorrent/vesper/combobox";
export default function ComboboxRefDemo() {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
ref.current?.focus();
}, []);
return (
<Combobox
ref={ref}
aria-label="Country"
placeholder="Select your country"
options={["Canada", "Japan", "Norway"]}
/>
);
}