Monet
@etched/monet 0.0.1

Select

Ported

Pick one value (or several with multiple) from a list. readOnly keeps the value but still opens the list to show the options. Native <button> attributes and ref pass through to the trigger.

Kumo source
import { Select } from "@etched/monet/select";

const sites = [
  { value: "tpe-2", label: "TPE-2" },
  { value: "sjc-1", label: "SJC-1" },
  { value: "aus-3", label: "AUS-3 (offline)", disabled: true },
];

export function Preview() {
  return <Select aria-label="Site" items={sites} placeholder="Choose a site" />;
}

Installation

import { Select, SelectGroup, SelectGroupLabel, SelectOption, SelectSeparator } from "@etched/monet/select";

Examples

Basics

import { Select } from "@etched/monet/select";

const statuses = { queued: "Queued", running: "Running", passed: "Passed", failed: "Failed" };

const sites = [
  { value: "tpe-2", label: "TPE-2" },
  { value: "sjc-1", label: "SJC-1" },
  { value: "aus-3", label: "AUS-3 (offline)", disabled: true },
];

export function Basics() {
  return (
    <div className="row">
      <Select aria-label="Status" items={statuses} defaultValue="running" />
      <Select aria-label="Site" items={sites} placeholder="Choose a site" />
      <Select aria-label="Status, small" size="sm" items={statuses} defaultValue="passed" />
      <Select aria-label="Status, disabled" items={statuses} defaultValue="queued" disabled />
    </div>
  );
}

Pill (status in a table)

The trigger takes the color of the selected option's tone or hue.

import { Select } from "@etched/monet/select";

const statuses = { queued: "Queued", running: "Running", passed: "Passed", failed: "Failed" };

const stages = [
  { value: "todo", label: "To Do", tone: "neutral" },
  { value: "scheduling", label: "Scheduling", tone: "orange" },
  { value: "scheduled", label: "Scheduled", tone: "purple" },
  { value: "touch", label: "In Touch", tone: "teal" },
  { value: "interviewing", label: "Interviewing", tone: "cyan" },
  { value: "references", label: "References check", tone: "cyan" },
  { value: "hired", label: "Hired", tone: "green" },
  { value: "passed", label: "We Passed", tone: "red" },
  { value: "trial", label: "Work trial (custom)", hue: 236 },
] as const;

export function PillStatusInATable() {
  return (
    <div className="row">
      <Select aria-label="Stage" appearance="pill" items={stages} defaultValue="interviewing" />
      <Select aria-label="Stage, hired" appearance="pill" items={stages} defaultValue="hired" />
      <Select aria-label="Stage, custom" appearance="pill" items={stages} defaultValue="trial" />
      <Select aria-label="Status, pill" appearance="pill" items={statuses} defaultValue="running" />
    </div>
  );
}

In a form

Status

Runs in Failed state are retried at 02:00.

Site (optional)
Owner
Choose an owner before you save.
import { Select } from "@etched/monet/select";

const statuses = { queued: "Queued", running: "Running", passed: "Passed", failed: "Failed" };

const sites = [
  { value: "tpe-2", label: "TPE-2" },
  { value: "sjc-1", label: "SJC-1" },
  { value: "aus-3", label: "AUS-3 (offline)", disabled: true },
];

export function InAForm() {
  return (
    <div className="grid">
      <Select label="Status" items={statuses} defaultValue="running" description="Runs in Failed state are retried at 02:00." />
      <Select label="Site" items={sites} placeholder="Choose a site" required={false} />
      <Select label="Owner" items={{ ana: "Ana Ruiz", sam: "Sam Lee" }} placeholder="Choose an owner" error="Choose an owner before you save." />
    </div>
  );
}

Descriptions

Priority
import { Select } from "@etched/monet/select";

const priorities = [
  { value: "p0", label: "P0", description: "Drop everything and page the owner." },
  { value: "p1", label: "P1", description: "Fix in this sprint." },
  { value: "p2", label: "P2", description: "Fix when there is time." },
];

export function Descriptions() {
  return (
    <div className="grid">
      <Select label="Priority" items={priorities} defaultValue="p1" />
    </div>
  );
}

Loading and read-only

loading shows a pulsing line and disables the trigger while the options come from the server. readOnly keeps the value; the list still opens to show the options.

Approver
Status

Set by the bench. You cannot change it here.

import { Select } from "@etched/monet/select";

const statuses = { queued: "Queued", running: "Running", passed: "Passed", failed: "Failed" };

export function LoadingAndReadOnly() {
  return (
    <div className="grid">
      <Select label="Approver" loading items={[]} placeholder="Choose an approver" />
      <Select label="Status" readOnly items={statuses} defaultValue="running" description="Set by the bench. You cannot change it here." />
    </div>
  );
}

Groups and multiple

import { Select, SelectGroup, SelectGroupLabel, SelectOption, SelectSeparator } from "@etched/monet/select";

const statuses = { queued: "Queued", running: "Running", passed: "Passed", failed: "Failed" };

export function GroupsAndMultiple() {
  return (
    <div className="row">
      <Select aria-label="Bench" placeholder="Choose a bench">
        <SelectGroup>
          <SelectGroupLabel>Thermal</SelectGroupLabel>
          <SelectOption value="b-14">B-14</SelectOption>
          <SelectOption value="b-15">B-15</SelectOption>
        </SelectGroup>
        <SelectSeparator />
        <SelectGroup>
          <SelectGroupLabel>Power</SelectGroupLabel>
          <SelectOption value="p-02">P-02</SelectOption>
        </SelectGroup>
      </Select>
      <Select
        aria-label="Statuses"
        multiple
        items={statuses}
        defaultValue={["failed", "running"]}
        placeholder="Any status"
        // SAFETY: the items are the statuses record, so each value is one of its keys.
        renderValue={(values) => (values.length === 1 ? statuses[values[0] as keyof typeof statuses] : `${values.length} statuses`)}
      />
    </div>
  );
}

Accessibility

  • Enter, Space or ArrowDown opens the list. In it, arrows and Home/End move, letters jump to an option, Enter picks, Escape closes.
  • The trigger is a combobox with aria-haspopup="listbox" and aria-expanded, named by label or aria-label. Options are options with aria-selected.
  • description and error describe the trigger, and error sets aria-invalid. loading makes it disabled and aria-busy; readOnly sets aria-readonly.
  • Give it a label, or an aria-label where no label shows, such as in a table cell.

API reference

SelectGroupLabel

PropTypeDefaultDescription
classNamestring–

SelectGroup

PropTypeDefaultDescription
classNamestring–

SelectOption

PropTypeDefaultDescription
classNamestring–
level1 | 2 | 3 | 4–A step on the green ramp, for an ordinal score such as conviction 1–4. Wins over hue and tone. A Pill with a level is a small score box, and a list option with one takes the fill across its row.
value *T–
children *ReactNode–
descriptionReactNode–Second line under the label.

Select

PropTypeDefaultDescription
labelReactNode–Visible label. Without one, give the control an aria-label.
labelTooltipReactNode–More about the field, behind an info button after the label.
labelTooltipLabelstring"More information"Names the info button.
descriptionReactNode–Help text below the control. Hidden while an error shows.
errorFieldError–
itemsReadonlyArray<SelectItem<T>> | ([T] extends [string] ? Record<string, ReactNode> : never)–Options as { label, value } objects; for string values, also a { value: label } record. Or pass SelectOption children.
childrenReactNode–
size"sm" | "base""base"Same heights as Input.
appearance"default" | "pill" | "ghost" | "box""default"pill is a rounded, borderless trigger for statuses in tables (Orbit\'s status dropdown). ghost has no edge or fill until hover, for values in table cells; it takes the selected option\'s ink.
placeholderReactNode–Shown in the trigger while nothing is selected.
renderValue(value: Multiple extends true ? T[] : T) => ReactNode–Custom trigger text for the selected value (or values when multiple).
classNamestring–Class on the trigger, for layout such as width.
sideBaseSelect.Positioner.Props["side"]"bottom"Which side of the trigger the list opens on.
alignBaseSelect.Positioner.Props["align"]"start"
alignItemWithTriggerbooleanfalseOverlay the list on the trigger like a native select.
loadingboolean–While the options load from the server: a pulsing line in place of the value, and the trigger is disabled and busy (Kumo).
containerBaseSelect.Portal.Props["container"]document.bodyThe element the list's portal goes into, for example a shadow root.

SelectSeparator

PropTypeDefaultDescription
classNamestring–