Propel

Combobox Field

A labeled multi-select combobox with title and description above a closed trigger, an overlay panel of searchable checkbox items, and helper or error text below.

Filter the deployment region.

Helper text

Show code
import { ComboboxField } from "@makeplane/propel/components/combobox-field";
import { Icon } from "@makeplane/propel/components/icon";
import { AddOutline } from "@makeplane/propel/icons";

const REGIONS = ["us-central-1", "us-east-1", "eu-central-1", "ap-west-1"];

export default function BasicDemo() {
  return (
    <ComboboxField
      name="region"
      label="Region"
      description="Filter the deployment region."
      hint="Helper text"
      required
      size="lg"
      items={REGIONS}
      placeholder="Select label"
      empty="No regions found"
      icon={<Icon icon={AddOutline} tint="placeholder" />}
    />
  );
}

Installation

import { ComboboxField } from "@makeplane/propel/components/combobox-field";

Usage

import { ComboboxField } from "@makeplane/propel/components/combobox-field";
import { Icon } from "@makeplane/propel/components/icon";
import { AddOutline } from "@makeplane/propel/icons";

const REGIONS = ["us-central-1", "us-east-1", "eu-central-1", "ap-west-1"];

export default function BasicDemo() {
  return (
    <ComboboxField
      name="region"
      label="Region"
      description="Filter the deployment region."
      hint="Helper text"
      required
      size="lg"
      items={REGIONS}
      placeholder="Select label"
      empty="No regions found"
      icon={<Icon icon={AddOutline} tint="placeholder" />}
    />
  );
}

Options and selection

Pass primitive options directly, or records with both getItemValue and getItemLabel (required for non-primitive items). value and onValueChange use unique string/number IDs; filter receives the original options. Prefer stable accessor references for large lists.

The field picks one value by default: value is a single ID (or null), rows show a check, and the popup closes on pick. Pass multiple to pick several — see Multiple selection.

Large lists and server results

Rows are virtualized by default: the popup stays trigger-width and long labels truncate (full text as a tooltip). virtualized={false} mounts every option and sizes the popup to its content. For huge catalogs, supply externally filtered results through filteredItems; virtualization bounds rendering, not local search cost. Fetching and pagination remain application-owned. Use inputValue and onInputValueChange to control the search query.

Keep known records in items and offered results in filteredItems. For off-page selection labels, retain those records in items or supply itemToStringLabel(id); otherwise the ID is displayed. The field does not retain previous results. With multiple, Select all affects filteredItems when provided (including an empty array), otherwise all items, and preserves selections outside that list.

Examples

Multiple selection

multiple picks several values: value and onValueChange use an array of IDs, rows show a checkbox, and a sticky Select all row selects or clears every option. The trigger previews three labels plus a +N more count (or N selected when maxVisible={0}). Set maxVisible to change the preview, overflowLabel to localize the count, and selectAllLabel to rename the row.

Show code
import { ComboboxField } from "@makeplane/propel/components/combobox-field";

const REGIONS = ["us-central-1", "us-east-1", "eu-central-1", "ap-west-1", "ap-southeast-2"];

export default function MultipleDemo() {
  return (
    <ComboboxField
      multiple
      name="regions"
      label="Regions"
      size="lg"
      items={REGIONS}
      defaultValue={["us-east-1", "eu-central-1"]}
      placeholder="Select regions"
      empty="No regions found"
    />
  );
}

Record options

Selection is ID-based. Supply getItemValue / getItemLabel so labels can repeat while IDs stay unique.

Selection uses member IDs; labels come from getItemLabel.

Both accessors are required for record options.

Show code
import { ComboboxField } from "@makeplane/propel/components/combobox-field";
import { Icon } from "@makeplane/propel/components/icon";
import { AddOutline } from "@makeplane/propel/icons";
import * as React from "react";

const members = [
  { id: 7, name: "Alex", email: "engineering@example.com" },
  { id: 42, name: "Priya", email: "design@example.com" },
  { id: 99, name: "Marcus", email: "ops@example.com" },
];
type Member = (typeof members)[number];

// Module-level accessors stay referentially stable, which large lists rely on.
const memberId = (member: Member) => member.id;
const memberName = (member: Member) => member.name;

export default function RecordsDemo() {
  const [ids, setIds] = React.useState<number[]>([7]);
  return (
    <ComboboxField
      multiple
      name="members"
      label="Members"
      description="Selection uses member IDs; labels come from getItemLabel."
      hint="Both accessors are required for record options."
      size="lg"
      items={members}
      getItemValue={memberId}
      getItemLabel={memberName}
      value={ids}
      onValueChange={setIds}
      placeholder="Select members"
      empty="No members found"
      icon={<Icon icon={AddOutline} tint="placeholder" />}
    />
  );
}

Rich options

getItemProps passes extra ComboboxItem props to each option’s row: a leading icon, a trailing count, disabled, and so on. The field still sets each row’s value, label and checkbox itself. footer pins content under the list.

Show code
import { Badge } from "@makeplane/propel/components/badge";
import { ComboboxField } from "@makeplane/propel/components/combobox-field";
import { Icon } from "@makeplane/propel/components/icon";
import { GlobeOutline, ServerOutline } from "@makeplane/propel/icons";

const services = [
  { id: "api", name: "API gateway", instances: 12 },
  { id: "edge", name: "Edge cache", instances: 4 },
  { id: "legacy", name: "Legacy queue", instances: 0 },
];
type Service = (typeof services)[number];

// Module-level accessors stay referentially stable, which large lists rely on.
const serviceId = (service: Service) => service.id;
const serviceName = (service: Service) => service.name;
const serviceRow = (service: Service) => ({
  icon: <Icon icon={service.id === "edge" ? GlobeOutline : ServerOutline} tint="secondary" />,
  trailing: <Badge size="xs" variant="neutral" label={String(service.instances)} />,
  disabled: service.instances === 0,
});

export default function RichOptionsDemo() {
  return (
    <ComboboxField
      multiple
      name="services"
      label="Services"
      size="lg"
      items={services}
      getItemValue={serviceId}
      getItemLabel={serviceName}
      getItemProps={serviceRow}
      placeholder="Select services"
      empty="No services found"
      footer={<div className="px-3 py-2 text-caption-md-regular text-tertiary">3 services</div>}
    />
  );
}

Sizes

size steps the label, description, and helper text together with the trigger’s own scale.

Filter the deployment region.

Helper text

Filter the deployment region.

Helper text

Filter the deployment region.

Helper text

Show code
import { ComboboxField } from "@makeplane/propel/components/combobox-field";

const REGIONS = ["us-central-1", "us-east-1", "eu-central-1", "ap-west-1"];
const SIZES = ["lg", "xl", "2xl"] as const;

export default function SizesDemo() {
  return (
    <div className="flex flex-col gap-4">
      {SIZES.map((size) => (
        <ComboboxField
          key={size}
          name={`region-${size}`}
          label={size}
          description="Filter the deployment region."
          hint="Helper text"
          size={size}
          items={REGIONS}
          placeholder="Select label"
          empty="No regions found"
        />
      ))}
    </div>
  );
}

Trigger width

width="auto" is the default and preserves the existing 288px minimum. Set width="fit" to make a secondary or ghost trigger hug its rendered content. To keep the search panel usable, ComboboxField gives a fit trigger content-based popup sizing by default: auto-fixed while the list is virtualized and auto otherwise. An explicit sizing prop still takes precedence. Pill remains intrinsically sized and does not accept width.

Show code
import {
  ComboboxField,
  type ComboboxTriggerWidth,
} from "@makeplane/propel/components/combobox-field";

const REGIONS = ["us-east-1", "A considerably longer deployment region"];
const WIDTHS: ComboboxTriggerWidth[] = ["auto", "fit"];

export default function FitWidthDemo() {
  return (
    <div className="flex flex-wrap items-end gap-4">
      {WIDTHS.map((width) => (
        <ComboboxField
          key={width}
          name={`region-${width}`}
          label={width}
          size="lg"
          width={width}
          empty="No regions found"
          items={REGIONS}
          defaultValue="us-east-1"
        />
      ))}
    </div>
  );
}

Pill

Passing variant="pill" makes the trigger hug its selection for compact filter pickers, like SelectField’s pill. It follows Propel Pill anatomy and does not add a chevron. The popup gets content-based sizing so the options stay readable. Pill accepts xs, sm, and md; the label and helper text stay on the field’s md typography rung.

Show code
import { ComboboxField } from "@makeplane/propel/components/combobox-field";

const REGIONS = ["us-central-1", "us-east-1", "eu-central-1", "ap-west-1"];

export default function PillDemo() {
  return (
    <ComboboxField
      multiple
      name="regions"
      label="Regions"
      variant="pill"
      size="md"
      items={REGIONS}
      placeholder="Any region"
      empty="No regions found"
    />
  );
}

Invalid

Setting error marks the field invalid, recoloring the trigger border. The error replaces helper text below the control; the description above the control stays.

Choose a deployment region.
Show code
import { ComboboxField } from "@makeplane/propel/components/combobox-field";

const REGIONS = ["us-central-1", "us-east-1", "eu-central-1", "ap-west-1"];

export default function InvalidDemo() {
  return (
    <ComboboxField
      name="region"
      label="Region"
      error="Choose a deployment region."
      size="lg"
      items={REGIONS}
      placeholder="Select label"
      empty="No regions found"
    />
  );
}

Disabled

Filter the deployment region.

Helper text

Show code
import { ComboboxField } from "@makeplane/propel/components/combobox-field";
import { Icon } from "@makeplane/propel/components/icon";
import { AddOutline } from "@makeplane/propel/icons";

const REGIONS = ["us-central-1", "us-east-1", "eu-central-1", "ap-west-1"];

export default function DisabledDemo() {
  return (
    <ComboboxField
      name="region"
      label="Region"
      description="Filter the deployment region."
      hint="Helper text"
      size="lg"
      disabled
      items={REGIONS}
      placeholder="Select label"
      empty="No regions found"
      icon={<Icon icon={AddOutline} tint="placeholder" />}
    />
  );
}

API Reference

ComboboxField

Ready-to-use combobox field: a closed trigger, overlay search, option rows, and helper/error text. Picks one value by default; multiple picks several, with checkbox rows and Select all. Record options require getItemValue and getItemLabel. Primitive string/number options use themselves as IDs when those accessors are omitted. For large lists, keep accessors and itemToStringLabel referentially stable; supply a new items array when what they return for an item changes.

PropTypeDefaultDescription
size(required)"xs" | "sm" | "md" | "lg" | "xl" | "2xl"—Trigger size on Pill's scale: xs, sm or md. Trigger size, from md to 2xl: height, text and glyph sizing.
items(required)readonly Item[] | (readonly Item[] & readonly { items: readonly Item[]; }[])—Known flat options: primitive IDs, or records with explicit value/label accessors. Selected values are always IDs. Keep off-page selected records here to resolve their labels, or supply itemToStringLabel(id) as a fallback. Records must not have an array-valued items property (reserved for Base UI groups). This field does not fetch or retain past records.
empty(required)ReactNode—Message rendered when no item matches.
getItemValue(required)(item: Item) => Value—Required for record items: derive a stable unique ID. Omit for string/number items, which are their own IDs.
getItemLabel(required)(item: Item) => string—Required for record items: derive the display/filter label (labels may repeat). Omit for string/number items, which use itemToStringLabel or String.
autoCompletestring—Provides a hint to the browser for autofill. @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete
autoHighlightbooleanfalseWhether the first matching item is highlighted automatically while filtering.
highlightItemOnHoverbooleantrueWhether moving the pointer over items should highlight them. Disabling this prop allows CSS :hover to be differentiated from the :focus (data-highlighted) state.
itemToStringLabel((itemValue: Value) => string)—When the item values are objects (<Combobox.Item value={object}>), this function converts the object value to a string representation for display in the input. If the shape of the object is { value, label }, the label will be used automatically without needing to specify this prop. With a createItems() collection, this receives the derived value, and the collection's getLabel takes precedence for values it can resolve.
itemToStringValue((itemValue: Value) => string)—When the item values are objects (<Combobox.Item value={object}>), this function converts the object value to a string representation for form submission. If the shape of the object is { value, label }, the value will be used automatically without needing to specify this prop. With a createItems() collection, this receives the derived value.
actionsRefRefObject<Actions | null>—A ref to imperative actions. - unmount: Manually unmounts the combobox. Call this after any externally controlled closing animation finishes.
onOpenChange((open: boolean, eventDetails: ChangeEventDetails) => void)—Event handler called when the popup is opened or closed.
onInputValueChange((inputValue: string, eventDetails: ChangeEventDetails) => void)—Event handler called when the input value changes.
onItemHighlighted((highlightedValue: Value, eventDetails: HighlightEventDetails) => void)—Callback fired when an item is highlighted or unhighlighted. Receives the highlighted item value (or undefined if no item is highlighted) and event details with a reason property describing why the highlight changed. The reason can be: - 'keyboard': the highlight changed due to keyboard navigation. - 'pointer': the highlight changed due to pointer hovering. - 'none': the highlight changed programmatically.
modalbooleantrueWhether the combobox enters a modal state when open, like Select: page scroll is locked and a press outside the popup only dismisses it. The input (or input group) is cut out of the backdrop, so filter-in-field compositions still take typing and clicks. Set false to leave the rest of the page interactive while the list is open.
iconReactNode—Decorative leading element at the inline-start, e.g. <Icon icon={Plus} tint="placeholder" />.
trailingReactNode—Decorative trailing glyph. Omit it to show a chevron (the Figma end node) — except on pill, which follows Pill anatomy and shows no glyph unless one is passed. This is not a nested popup-trigger control — the frame itself is the Trigger.
placeholderReactNode—Shown in the trigger while nothing is selected — Base UI's Combobox.Value placeholder.
footerReactNode—Sticky chrome pinned below the list.
variant"secondary" | "ghost" | "pill""secondary"pill hugs its value with outline Pill chrome and shows no automatic chevron. Frame look. secondary is the bordered field surface; ghost is borderless. Invalid on ghost has no trigger chrome — the danger cue is the helper text below.
width"auto" | "fit"—auto keeps the 288px minimum; fit hugs the rendered trigger content.
filteredItemsreadonly Item[]—Externally filtered flat options offered in the popup. When supplied, bulk actions affect only these records, not all known items. Pass [] for an empty response. Omit for local filtering; local search does not change the bulk-action scope. Fetching and pagination are caller-owned.
multiplebooleanfalseAllow several selections: checkbox rows, a Select all row, and a trigger that previews the first maxVisible labels. Off (the default) picks one value, with check rows, and closes the popup on pick. Fixed for a mounted field: the value's shape follows it, so switching modes means remounting the field (e.g. with a new key).
valuestring | number | Value[] | null—The selected values (controlled). The selected value (controlled); null for none.
defaultValuestring | number | Value[] | null—The initially selected values (uncontrolled). The initially selected value (uncontrolled).
onValueChange((value: Value[], details?: ChangeEventDetails) => void) | ((value: Value | null, details: ChangeEventDetails) => void)—Called when the selection changes. Item presses pass Base UI's event details; Select all passes only the next value. Called when the selection changes, with Base UI's event details.
maxVisiblenumber3Maximum selected labels to preview. A nonnegative integer; zero shows only the count. Hidden labels are not formatted. Long previews truncate without hiding the overflow count.
overflowLabel((hiddenCount: number, details: { visibleCount: number; }) => ReactNode)(hiddenCount, { visibleCount }) => visibleCount > 0 ? `+${hiddenCount} more` : `${hiddenCount} selected`Non-interactive summary of selections beyond maxVisible. Receives the hidden count and how many labels remain in the preview. Override to localize.
selectAllLabelstringSelect allLabel for the sticky row that selects or clears every item. Acts on the whole items list, NOT on whatever the panel search has filtered down to — so pressing it while a query is active also selects the values the query hid, and the row's checked / indeterminate state likewise reflects items rather than the visible subset. When filteredItems is supplied it defines the offered list instead. Other selections are preserved when selecting or clearing this list. This does not select unseen server results; use a label such as "Select loaded members" for remote pages.
virtualizedbooleantrueMount only visible options and a small buffer. The popup stays at the anchor width and caps at 288px tall (sizing="anchor-fixed") so a mid-open height settle cannot shrink under the virtualizer. Set false only when every option must remain mounted; rendering then grows with the list and the popup uses available height.
sizing"menu" | "anchor" | "sm" | "md" | "lg" | "auto" | "anchor-fixed" | "auto-fixed"—How the popup takes its width (see ComboboxContent). Defaults to anchor-fixed for a virtualized list and anchor otherwise. A pill trigger defaults to the content's width instead (auto-fixed virtualized, auto otherwise), so the options are not squeezed to its compact width.
descriptionReactNode—Supporting text shown below the label.
getItemProps((item: Item) => ComboboxFieldItemProps)—Extra ComboboxItem props for an option's row, e.g. { icon, trailing, disabled }. The field sets each row's value, label and selection marker itself. Select all skips a disabled row: it neither selects nor clears it. That scope calls this for every offered option, so keep it referentially stable for large lists.
errorReactNode—Error text shown below the control.
hintReactNode—Helper text shown below the control. Replaced by error when an error is set.
searchLabelstringSearchAccessible name and placeholder for the panel search field.
labelstring—
aria-labelstring—