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}
    />
  );
}
Always give a 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

PropTypeDescriptionDefault
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"
placeholderstringPlaceholder text shown in the input when it is empty."Search..."
emptyStateTextstringThe text displayed in the dropdown when no options match the input's value."No results"
valuestring | nullThe currently selected value (controlled mode). Pass null to represent no selection.—
defaultValuestring | nullThe initial selected value (uncontrolled mode).—
onValueChange(value: string | null) => voidCallback invoked with the new value when the selection changes, or null when it is cleared.—
inputValuestringThe current text displayed in the input (controlled mode).—
defaultInputValuestringThe initial text displayed in the input (uncontrolled mode).—
onInputValueChange(value: string) => voidCallback invoked with the new text whenever the input's value changes.—
openbooleanControls the open state of the dropdown (controlled mode).—
defaultOpenbooleanWhether the dropdown is open by default (uncontrolled mode).false
onOpenChange(open: boolean) => voidCallback fired when the open state of the dropdown changes.—
namestringThe form field name submitted with form data.—
formstringAssociates the combobox with a <form> element by its id.—
idstringAn identifier applied to the underlying underlying <input> element.—
requiredbooleanWhen true, marks the input as required for native form validation.false
disabledbooleanWhen true, prevents interaction with the input and dropdown.false
readOnlybooleanWhen true, the input's value cannot be edited, but it is still submitted with form data.false
containerHTMLElement | ShadowRoot | null | RefObject<HTMLElement | ShadowRoot | null>Specify the element or shadow root to portal the dropdown into.—
refRef<HTMLInputElement>A ref forwarded to the underlying <input> element.—
clearButtonAriaLabelstringAccessible label for the button that clears the currently selected value."Clear selection"
dropdownTriggerAriaLabelstringAccessible 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

PropertyTypeDescription
labelstringThe text displayed for this option in the dropdown, and used to filter options as the user types.
valuestringThe 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}
    />
  );
}
Options are always filtered against the text in the input. When you supply options that have already been filtered by a server, make sure the results have labels that match against what the user typed.

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"]}
    />
  );
}