Choicebox

A card-style selection component supporting both single-select and multi-select modes. Each option displays a label and optional description with a visual checkmark indicator.

import { Choicebox } from "@tenstorrent/vesper/choicebox";

export default function ChoiceboxDemo() {
  return (
    <Choicebox
      aria-label="Payment plan"
      name="plan"
      defaultValue="pro"
      options={[
        { value: "starter", label: "Starter", description: "For individuals" },
        { value: "pro", label: "Pro", description: "For small teams" },
        {
          value: "enterprise",
          label: "Enterprise",
          description: "Custom solutions",
        },
      ]}
    />
  );
}
Give the group an accessible name so assistive technology can announce what the options belong to. Choicebox renders a <fieldset>, so you can either pass it an aria-label, or point aria-labelledby at the heading or label that already names the group.

Options

PropTypeDescriptionDefault
namestringThe name attribute shared by all inputs in the group, used for form submission.—
optionsChoiceboxItem[]The list of options to render. Each has value, label, optional description, disabled, and id.—
disabledbooleanWhen true, disables all options in the group.false
multiselectbooleanWhen true, enables multi-select (checkbox) mode. Otherwise single-select (radio).false
valuestringThe currently selected value (single-select, controlled).—
defaultValuestringThe initially selected value (single-select, uncontrolled).—
valuesstring[]The currently selected values (multi-select, controlled).—
defaultValuesstring[]The initially selected values (multi-select, uncontrolled).—
onChange(value: string) => void | (values: string[]) => voidCallback fired when the selection changes.—
requiredbooleanWhen true, a selection is required (single-select only).false
minnumberMinimum selections required (multi-select only).0
maxnumberMaximum selections allowed (multi-select only).Infinity

All other props are forwarded to the wrapping <fieldset> element.

ChoiceboxItem options

PropertyTypeDescription
valuestringThe value submitted with form data and passed to onChange. Must be unique.
labelstringThe text displayed for this option.
descriptionstringAn optional secondary line of text displayed below the label.
disabledbooleanWhen true, prevents this option from being selected. Defaults to false.
idstringAn id applied to this option's <input>, used to reference it from validation errors.

Use a Choicebox when each option benefits from a description and a large, card-style target. For a plain list of mutually exclusive options, reach for the RadioGroup component, and for a single boolean choice, reach for the Checkbox component.

Examples

Defining options

Render a Choicebox by passing an array of ChoiceboxItem objects as the options prop. Each item describes a single card. value is what gets submitted and reported to onChange, label is the text displayed on the card, and the optional description adds a secondary line below it:

import { Choicebox } from "@tenstorrent/vesper/choicebox";

export default function ChoiceboxOptions() {
  return (
    <Choicebox
      aria-label="Region"
      name="region"
      options={[
        {
          value: "us",
          label: "United States",
          description: "Lowest latency in North America",
        },
        {
          value: "eu",
          label: "Europe",
          description: "Data stays within the EU",
        },
        { value: "ap", label: "Asia Pacific" },
      ]}
    />
  );
}

Options without a description render as a more compact card, as seen with the "Asia Pacific" option above.

Single-select

By default, a Choicebox operates in single-select mode, rendering each option as a radio input so that picking one deselects the others. Pass defaultValue to select an option initially:

import { Choicebox } from "@tenstorrent/vesper/choicebox";

export default function UncontrolledChoicebox() {
  return (
    <Choicebox
      aria-label="Payment plan"
      name="plan"
      defaultValue="monthly"
      options={[
        { value: "monthly", label: "Monthly", description: "$12 per month" },
        {
          value: "yearly",
          label: "Yearly",
          description: "$120 per year, two months free",
        },
      ]}
    />
  );
}

To control the selection, pass value alongside onChange, which receives the newly selected value:

Selected value: monthly

import { useState } from "react";

import { Choicebox } from "@tenstorrent/vesper/choicebox";
import { Typography } from "@tenstorrent/vesper/typography";

export default function ControlledChoicebox() {
  const [value, setValue] = useState("monthly");

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "var(--vesper-spacing-4)",
      }}
    >
      <Choicebox
        aria-label="Payment plan"
        name="plan"
        value={value}
        onChange={setValue}
        options={[
          { value: "monthly", label: "Monthly", description: "$12 per month" },
          { value: "yearly", label: "Yearly", description: "$120 per year" },
        ]}
      />
      <Typography variant="copy-sm">Selected value: {value}</Typography>
    </div>
  );
}

Multi-select

Passing multiselect renders each option as a checkbox, allowing any number of them to be selected. In this mode the uncontrolled prop is defaultValues, and onChange receives the full array of selected values:

import { Choicebox } from "@tenstorrent/vesper/choicebox";

export default function MultiSelectChoicebox() {
  return (
    <Choicebox
      multiselect
      aria-label="Features"
      name="features"
      defaultValues={["dark-mode"]}
      options={[
        {
          value: "dark-mode",
          label: "Dark mode",
          description: "Follow the system colour scheme",
        },
        {
          value: "notifications",
          label: "Notifications",
          description: "Email me when a job finishes",
        },
        {
          value: "analytics",
          label: "Analytics",
          description: "Share anonymous usage data",
        },
      ]}
    />
  );
}

Controlled multi-select works the same way as single-select, except that you pass values instead of value:

Selected values: analytics

import { useState } from "react";

import { Choicebox } from "@tenstorrent/vesper/choicebox";
import { Typography } from "@tenstorrent/vesper/typography";

export default function ControlledMultiSelectChoicebox() {
  const [values, setValues] = useState<string[]>(["analytics"]);

  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "var(--vesper-spacing-4)",
      }}
    >
      <Choicebox
        multiselect
        aria-label="Features"
        name="features"
        values={values}
        onChange={setValues}
        options={[
          { value: "notifications", label: "Notifications" },
          { value: "analytics", label: "Analytics" },
        ]}
      />
      <Typography variant="copy-sm">
        Selected values: {values.join(", ") || "none"}
      </Typography>
    </div>
  );
}

Disabling options

Pass disabled to an individual option to prevent it from being selected while leaving the rest of the group interactive. You can also pass disabled to the Choicebox component itself to disable every option at once:

import { Choicebox } from "@tenstorrent/vesper/choicebox";

export default function DisabledChoiceboxOptions() {
  return (
    <div
      style={{
        display: "flex",
        flexDirection: "column",
        gap: "var(--vesper-spacing-8)",
      }}
    >
      <Choicebox
        aria-label="Payment plan"
        name="disabled-option"
        defaultValue="pro"
        options={[
          { value: "pro", label: "Pro", description: "Available now" },
          {
            value: "enterprise",
            label: "Enterprise",
            description: "Contact sales to enable",
            disabled: true,
          },
        ]}
      />
      <Choicebox
        disabled
        aria-label="Payment plan"
        name="disabled-group"
        defaultValue="pro"
        options={[
          { value: "pro", label: "Pro", description: "Available now" },
          { value: "starter", label: "Starter", description: "Available now" },
        ]}
      />
    </div>
  );
}

Usage in forms

Choicebox renders native inputs inside a <fieldset>, so it works with regular form submission and native validation. Every selected option submits its value under the group's name, which means a multi-select group can submit several values at once:

import { useState } from "react";

import { Button } from "@tenstorrent/vesper/button";
import { Choicebox } from "@tenstorrent/vesper/choicebox";
import { Typography } from "@tenstorrent/vesper/typography";

export default function FormChoiceboxDemo() {
  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(data.getAll("addons").join(", ") || "none");
      }}
    >
      <Choicebox
        multiselect
        aria-label="Addons"
        min={1}
        max={2}
        name="addons"
        options={[
          { value: "support", label: "Priority support" },
          { value: "backups", label: "Daily backups" },
          { value: "sso", label: "Single sign-on" },
        ]}
      />
      <Button size="sm" type="submit">
        Submit
      </Button>
      {submitted !== null && (
        <Typography variant="copy-sm">
          Submitted values: addons={submitted}
        </Typography>
      )}
    </form>
  );
}

The min and max props set a custom validation message on the group, so the form above refuses to submit until between one and two add-ons are selected. In single-select mode, pass required instead to require that one of the options is picked:

import { Choicebox } from "@tenstorrent/vesper/choicebox";

export default function RequiredChoicebox() {
  return (
    <Choicebox
      required
      aria-label="Payment plan"
      name="required-plan"
      options={[
        { value: "monthly", label: "Monthly" },
        { value: "yearly", label: "Yearly" },
      ]}
    />
  );
}