Propel

Combobox

A filterable input that selects one or more options from a list in a popup.

Basic

Show code
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInputGroup,
  ComboboxItem,
  ComboboxList,
} from "@makeplane/propel/components/combobox";
import { Field, FieldLabel } from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ChevronsUpDown, X } from "lucide-react";

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

export default function BasicDemo() {
  return (
    <Field name="region">
      <Combobox items={REGIONS}>
        <FieldLabel magnitude="md" inset={false}>
          Region
        </FieldLabel>
        <ComboboxInputGroup
          magnitude="md"
          placeholder="e.g. eu-central-1"
          clear={
            <IconButton
              prominence="ghost"
              tone="neutral"
              magnitude="md"
              aria-label="Clear region"
              icon={<Icon icon={X} />}
            />
          }
          trigger={
            <IconButton
              prominence="ghost"
              tone="neutral"
              magnitude="md"
              aria-label="Open region"
              icon={<Icon icon={ChevronsUpDown} />}
            />
          }
        />
        <ComboboxContent>
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} magnitude="md" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

Multiple

multiple swaps the input frame for ComboboxChips: each selected value becomes a removable chip ahead of the inline input.

Show code
import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
} from "@makeplane/propel/components/combobox";
import { Field, FieldLabel } from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { X } from "lucide-react";

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

export default function MultipleDemo() {
  return (
    <Field name="regions">
      <Combobox multiple items={REGIONS} defaultValue={["us-east-1", "eu-central-1"]}>
        <FieldLabel magnitude="md" inset={false}>
          Regions
        </FieldLabel>
        <ComboboxChips magnitude="md" placeholder="Add a region">
          {(region: string) => (
            <ComboboxChip
              key={region}
              label={region}
              remove={
                <IconButton
                  prominence="ghost"
                  tone="neutral"
                  magnitude="sm"
                  aria-label={`Remove ${region}`}
                  icon={<Icon icon={X} />}
                />
              }
            />
          )}
        </ComboboxChips>
        <ComboboxContent>
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} magnitude="md" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

Grouped

Show code
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxInputGroup,
  ComboboxItem,
  ComboboxList,
} from "@makeplane/propel/components/combobox";
import { Field, FieldLabel } from "@makeplane/propel/components/field";

const GROUPED_REGIONS = [
  { label: "Americas", items: ["us-central-1", "us-east-1", "sa-east-1"] },
  { label: "Europe", items: ["eu-central-1", "eu-west-1"] },
  { label: "Asia Pacific", items: ["ap-west-1", "ap-southeast-2"] },
];

export default function GroupedDemo() {
  return (
    <Field name="region">
      <Combobox items={GROUPED_REGIONS}>
        <FieldLabel magnitude="md" inset={false}>
          Region
        </FieldLabel>
        <ComboboxInputGroup magnitude="md" placeholder="e.g. eu-central-1" />
        <ComboboxContent>
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(group: (typeof GROUPED_REGIONS)[number]) => (
              <ComboboxGroup key={group.label} items={group.items}>
                <ComboboxGroupLabel>{group.label}</ComboboxGroupLabel>
                <ComboboxCollection>
                  {(region: string) => (
                    <ComboboxItem key={region} value={region} magnitude="md" label={region} />
                  )}
                </ComboboxCollection>
              </ComboboxGroup>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

filter={null} turns off built-in filtering so onInputValueChange can drive a remote search, with ComboboxStatus carrying the polite loading and empty-query hints.

Show code
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInputGroup,
  ComboboxItem,
  ComboboxList,
  ComboboxStatus,
  useFilter,
} from "@makeplane/propel/components/combobox";
import { Field, FieldLabel } from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { IconButton } from "@makeplane/propel/components/icon-button";
import { ChevronsUpDown, X } from "lucide-react";
import * as React from "react";

const MEMBERS = ["Aaditya Kapoor", "Bianca Ferreira", "Marcus Chen", "Priya Nair", "Rohan Sharma"];

export default function AsyncSearchDemo() {
  const [query, setQuery] = React.useState("");
  const [results, setResults] = React.useState<string[]>([]);
  const [pending, setPending] = React.useState(false);
  const timeoutRef = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
  const { contains } = useFilter();

  React.useEffect(() => () => clearTimeout(timeoutRef.current), []);

  const trimmed = query.trim();
  const status = pending ? "Searching…" : trimmed === "" ? "Start typing to search members" : null;

  return (
    <Field name="reviewer">
      <Combobox
        items={results}
        // Built-in filtering is off: the (fake) server already returns only the matches.
        filter={null}
        onInputValueChange={(nextQuery, { reason }) => {
          setQuery(nextQuery);
          if (reason === "item-press") {
            return;
          }
          clearTimeout(timeoutRef.current);
          const trimmedQuery = nextQuery.trim();
          if (trimmedQuery === "") {
            setResults([]);
            setPending(false);
            return;
          }
          setPending(true);
          // Deterministic stand-in for a server search: resolves after a short delay.
          timeoutRef.current = setTimeout(() => {
            setResults(MEMBERS.filter((member) => contains(member, trimmedQuery)));
            setPending(false);
          }, 300);
        }}
      >
        <FieldLabel magnitude="md" inset={false}>
          Reviewer
        </FieldLabel>
        <ComboboxInputGroup
          magnitude="md"
          placeholder="e.g. Priya"
          clear={
            <IconButton
              prominence="ghost"
              tone="neutral"
              magnitude="md"
              aria-label="Clear reviewer"
              icon={<Icon icon={X} />}
            />
          }
          trigger={
            <IconButton
              prominence="ghost"
              tone="neutral"
              magnitude="md"
              aria-label="Open reviewer"
              icon={<Icon icon={ChevronsUpDown} />}
            />
          }
        />
        <ComboboxContent>
          <ComboboxStatus>{status}</ComboboxStatus>
          <ComboboxEmpty>{!pending && trimmed !== "" ? "No members match" : null}</ComboboxEmpty>
          <ComboboxList>
            {(member: string) => (
              <ComboboxItem key={member} value={member} magnitude="md" label={member} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

Installation

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInputGroup,
  ComboboxItem,
  ComboboxList,
} from "@makeplane/propel/components/combobox";

Props

Combobox

PropTypeRequiredDescription
autoCompletestringNoProvides a hint to the browser for autofill. @see https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete
autoHighlightbooleanNoWhether the first matching item is highlighted automatically while filtering.
highlightItemOnHoverbooleanNoWhether 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)NoWhen 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.
itemToStringValue((itemValue: Value) => string)NoWhen 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.
isItemEqualToValue((itemValue: Value, value: Value) => boolean)NoCustom comparison logic used to determine if a combobox item value matches the current selected value. Useful when item values are objects without matching referentially. Defaults to `Object.is` comparison.
actionsRefRefObject<Actions | null>NoA ref to imperative actions. - `unmount`: Manually unmounts the combobox. Call this after any externally controlled closing animation finishes.
onOpenChange((open: boolean, eventDetails: ChangeEventDetails) => void)NoEvent handler called when the popup is opened or closed.
onInputValueChange((inputValue: string, eventDetails: ChangeEventDetails) => void)NoEvent handler called when the input value changes.
onItemHighlighted((highlightedValue: Value, eventDetails: HighlightEventDetails) => void)NoCallback 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.
multiplebooleanNoWhether multiple items can be selected.
defaultValueComboboxValueType<Value, Multiple> | nullNoThe uncontrolled selected value of the combobox when it's initially rendered. To render a controlled combobox, use the `value` prop instead.
valueComboboxValueType<Value, Multiple> | nullNoThe selected value of the combobox. Use when controlled.
onValueChange((value: ComboboxValueType<Value, Multiple> | (Multiple extends true ? never : null), eventDetails: ChangeEventDetails) => void)NoEvent handler called when the selected value of the combobox changes.

ComboboxInputGroup

PropTypeRequiredDescription
magnitude"sm" | "md" | "lg" | "xl"YesVisual size of the input row: height, text, and glyph sizing. Required.
iconReactNodeNoDecorative leading element at the inline-start, e.g. `<Icon icon={Search} tint="placeholder" />`.
clearReactElement<unknown, string | JSXElementConstructor<any>>NoThe clear control (e.g. an `IconButton`), grafted onto Base UI's `Clear` behavior. It carries its own — localizable — `aria-label`; the group bakes no label or glyph.
triggerReactElement<unknown, string | JSXElementConstructor<any>>NoThe popup-trigger control (e.g. an `IconButton`), grafted onto Base UI's `Trigger` behavior. It carries its own — localizable — `aria-label`; the group bakes no label or glyph.

ComboboxItem

PropTypeRequiredDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxItemState>NoAllows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a `ReactElement` or a function that returns the element to render.
magnitude"sm" | "md" | "lg"YesRow height and text size. Required.
labelstringYesOption label.