Select

A form-ready dropdown select component supporting controlled and uncontrolled usage with four visual variants.

import { Select } from "@tenstorrent/vesper/select";

export default function SelectDemo() {
  return (
    <Select
      aria-label="Fruit"
      placeholder="Select a fruit"
      options={[
        { value: "apple", label: "Apple" },
        { value: "banana", label: "Banana" },
        { value: "cherry", label: "Cherry" },
      ]}
    />
  );
}
Always give a Select 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(SelectItem | 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 select trigger. Affects height, padding, and typography."md"
variant"default" | "warning" | "success" | "error"The visual variant, which determines color scheme."default"
placeholderstringPlaceholder text shown in the trigger when no value is selected."Select an option"
iconReactNodeAn optional icon rendered at the leading edge of the trigger.—
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 whenever the selection changes, or null when it is cleared.—
namestringThe form field name submitted with form data.—
formstringAssociates the select with a <form> element by its id.—
idstringAn identifier applied to the trigger, used to reference it from form validation errors.—
requiredbooleanWhen true, marks the select as required for native form validation.false
disabledbooleanWhen true, prevents interaction with the trigger and dropdown.false
containerHTMLElement | ShadowRoot | null | RefObject<HTMLElement | ShadowRoot | null>Specify the element or shadow root to portal the dropdown into.—
refRef<HTMLButtonElement>A ref forwarded to the underlying select trigger <button> element.—

Form props (value, disabled, name, form, etc.) are forwarded to the hidden <input> element that holds the selected value. All other props are forwarded to the underlying <button> element.

SelectItem options

PropertyTypeDescription
labelstringThe text displayed for this option in the dropdown, and in the trigger once it is selected.
valuestringThe underlying value submitted with form data and passed to onValueChange. Must be unique.

A plain string option is shorthand for a SelectItem whose label and value are both that string.

If your list is long enough that users will want to search it, reach for the Combobox component instead: it renders the same trigger sizes and variants, but filters its options as the user types.

Examples

Defining options

Each entry in the options array can either be a SelectItem, or a plain string. Use SelectItem objects when the value associated with each item differs from the text you want to display:

import { Select } from "@tenstorrent/vesper/select";

export default function ObjectOptionsSelect() {
  return (
    <Select
      aria-label="Region"
      placeholder="Select a region"
      options={[
        { label: "United States", value: "us" },
        { label: "Europe", value: "eu" },
        { label: "Asia Pacific", value: "ap" },
      ]}
    />
  );
}

When the displayed text is the same as the value of the item, a string option is a convenient shorthand:

import { Select } from "@tenstorrent/vesper/select";

export default function StringOptionsSelect() {
  return (
    <Select
      aria-label="Country"
      placeholder="Select a country"
      options={["Canada", "Japan", "Norway"]}
    />
  );
}

Both shapes can be mixed in the same options array. Options are rendered in the order they are provided, and the selected one is marked with a checkmark when the dropdown is open.

Uncontrolled vs controlled

Render a Select 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 { Select } from "@tenstorrent/vesper/select";

export default function UncontrolledSelect() {
  return (
    <Select
      aria-label="Region"
      placeholder="Select a region"
      defaultValue="eu"
      options={[
        { label: "United States", value: "us" },
        { label: "Europe", value: "eu" },
        { label: "Asia Pacific", value: "ap" },
      ]}
    />
  );
}

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. There is no built-in way for a user to clear a selection, so pass null when you need to return the select to its placeholder state:

Selected value: apple

import { useState } from "react";

import { Button } from "@tenstorrent/vesper/button";
import { Select } from "@tenstorrent/vesper/select";
import { Typography } from "@tenstorrent/vesper/typography";

const FRUITS = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
  { label: "Mango", value: "mango" },
  { label: "Peach", value: "peach" },
];

export default function ControlledSelect() {
  const [fruit, setFruit] = useState<string | null>("apple");

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "var(--vesper-spacing-4)",
      }}
    >
      <Select
        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>
  );
}

Different sizes

The Select component can be rendered at sm, md, or lg size, defaulting to md. Size affects the height, padding, and text styles of the trigger, as well as the size of its icons.

import { Select } from "@tenstorrent/vesper/select";

const COUNTRIES = [
  { label: "Canada", value: "ca" },
  { label: "Japan", value: "jp" },
  { label: "Norway", value: "no" },
];

export default function SelectSizes() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "var(--vesper-spacing-4)",
      }}
    >
      <Select
        size="sm"
        aria-label="Country"
        placeholder="A small select"
        options={COUNTRIES}
      />
      <Select
        size="md"
        aria-label="Country"
        placeholder="A medium select"
        options={COUNTRIES}
      />
      <Select
        size="lg"
        aria-label="Country"
        placeholder="A large select"
        options={COUNTRIES}
      />
    </div>
  );
}

Rendering an icon

You can render an icon at the leading edge of the trigger via the icon prop. The icon is decorative, and stays in place as the selection changes:

import { Globe } from "@tenstorrent/vesper/icons";
import { Select } from "@tenstorrent/vesper/select";

export default function SelectWithIcon() {
  return (
    <Select
      aria-label="Region"
      icon={<Globe />}
      placeholder="Select a region"
      options={[
        { label: "United States", value: "us" },
        { label: "Europe", value: "eu" },
        { label: "Asia Pacific", value: "ap" },
      ]}
    />
  );
}
The trailing caret is rendered by the component itself, and flips between its open and closed states as the dropdown is toggled. Only the leading icon is configurable.

Disabling the select

Pass disabled to prevent all interaction with the trigger and its dropdown. A disabled Select still displays its current selection, but is skipped in the tab order and excluded from form data:

import { Select } from "@tenstorrent/vesper/select";

export default function DisabledSelect() {
  return (
    <Select
      disabled
      aria-label="Region"
      placeholder="Select a region"
      defaultValue="eu"
      options={[
        { label: "United States", value: "us" },
        { label: "Europe", value: "eu" },
        { label: "Asia Pacific", value: "ap" },
      ]}
    />
  );
}

Usage in forms

Select renders a hidden form control alongside its trigger, so passing a name includes the selected value in form submissions, and required participates in native form validation. Use the form prop to associate the field with a <form> rendered elsewhere on the page.

import { useState } from "react";

import { Button } from "@tenstorrent/vesper/button";
import { Select } from "@tenstorrent/vesper/select";
import { Typography } from "@tenstorrent/vesper/typography";

const FRUITS = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Cherry", value: "cherry" },
  { label: "Mango", value: "mango" },
  { label: "Peach", value: "peach" },
];

export default function FormSelectDemo() {
  const [submitted, setSubmitted] = useState<string | null>(null);

  return (
    <form
      className="gap-vesper-4 flex flex-col"
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        setSubmitted(String(data.get("fruit") ?? ""));
      }}
    >
      <Select
        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 label displayed in the trigger. The form above submits fruit=apple when "Apple" is selected.

Accessing the underlying element

Use the ref prop when you need direct access to the underlying trigger <button> element, eg. to focus it when a page loads:

import { useEffect, useRef } from "react";

import { Select } from "@tenstorrent/vesper/select";

export default function RegionSelect() {
  const ref = useRef<HTMLButtonElement>(null);

  useEffect(() => {
    ref.current?.focus();
  }, []);

  return (
    <Select
      aria-label="Region"
      placeholder="Select a region"
      options={["United States", "Europe", "Asia Pacific"]}
      ref={ref}
    />
  );
}