Propel

Combobox

A closed-field picker that selects one or more options from a searchable overlay panel — the same shell the dropdown menu uses.

Show code
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxSearch,
  ComboboxSelectAll,
  ComboboxSeparator,
  ComboboxStickyList,
  ComboboxTrigger,
} from "@makeplane/propel/components/combobox";
import { Field } from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { FieldLabel } from "@makeplane/propel/elements/field";
import { AddOutline } from "@makeplane/propel/icons";
import { useId, useState } from "react";

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

export default function BasicDemo() {
  const labelId = useId();
  const triggerId = useId();
  const [value, setValue] = useState<string[]>([]);
  const allSelected = REGIONS.every((region) => value.includes(region));
  const someSelected = !allSelected && value.length > 0;
  return (
    <Field name="region">
      <Combobox multiple items={REGIONS} value={value} onValueChange={setValue}>
        <FieldLabel id={labelId} size="lg" inset={false} htmlFor={triggerId}>
          Region
        </FieldLabel>
        <ComboboxTrigger
          id={triggerId}
          size="lg"
          placeholder="Select label"
          icon={<Icon icon={AddOutline} tint="placeholder" />}
        />
        <ComboboxContent
          aria-labelledby={labelId}
          search={
            <>
              <ComboboxSearch aria-label="Search" />
              <ComboboxStickyList>
                <ComboboxSelectAll
                  checked={allSelected}
                  indeterminate={someSelected}
                  onClick={() => setValue(allSelected ? [] : [...REGIONS])}
                />
                <ComboboxSeparator />
              </ComboboxStickyList>
            </>
          }
        >
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} selection="checkbox" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

Installation

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

Usage

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxSearch,
  ComboboxSelectAll,
  ComboboxSeparator,
  ComboboxStickyList,
  ComboboxTrigger,
} from "@makeplane/propel/components/combobox";
import { Field } from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { FieldLabel } from "@makeplane/propel/elements/field";
import { AddOutline } from "@makeplane/propel/icons";
import { useId, useState } from "react";

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

export default function BasicDemo() {
  const labelId = useId();
  const triggerId = useId();
  const [value, setValue] = useState<string[]>([]);
  const allSelected = REGIONS.every((region) => value.includes(region));
  const someSelected = !allSelected && value.length > 0;
  return (
    <Field name="region">
      <Combobox multiple items={REGIONS} value={value} onValueChange={setValue}>
        <FieldLabel id={labelId} size="lg" inset={false} htmlFor={triggerId}>
          Region
        </FieldLabel>
        <ComboboxTrigger
          id={triggerId}
          size="lg"
          placeholder="Select label"
          icon={<Icon icon={AddOutline} tint="placeholder" />}
        />
        <ComboboxContent
          aria-labelledby={labelId}
          search={
            <>
              <ComboboxSearch aria-label="Search" />
              <ComboboxStickyList>
                <ComboboxSelectAll
                  checked={allSelected}
                  indeterminate={someSelected}
                  onClick={() => setValue(allSelected ? [] : [...REGIONS])}
                />
                <ComboboxSeparator />
              </ComboboxStickyList>
            </>
          }
        >
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} selection="checkbox" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

Examples

Variants

variant sets the frame’s look on ComboboxTrigger, ComboboxInputGroup, ComboboxChips and ComboboxField, with the same names as SelectTrigger: secondary is the bordered field surface (the default), and ghost is borderless, filling only on hover, focus and while the popup is open. Use ghost where the control sits inside an already-bordered container — a toolbar or a table cell — so the frame does not double up.

ComboboxTrigger also takes pill, which keeps bordered chrome but hugs its value for compact property and filter pickers. Like Propel Pill, it has no automatic chevron: an open popup shows as its active chrome. Pair it with ComboboxContent sizing="auto" so the options are not squeezed to the compact trigger’s width. Its size scale is xs, sm and md, matching Propel Pill; the type contract rejects those compact sizes on secondary/ghost and rejects lg–2xl on pill.

Show code
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxSearch,
  ComboboxTrigger,
  type ComboboxTriggerVariant,
} from "@makeplane/propel/components/combobox";
import { Field } from "@makeplane/propel/components/field";
import { FieldLabel } from "@makeplane/propel/elements/field";
import { useId } from "react";

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

const VARIANTS: ComboboxTriggerVariant[] = ["secondary", "ghost", "pill"];

function VariantField({ variant }: { variant: ComboboxTriggerVariant }) {
  const labelId = useId();
  const triggerId = useId();
  return (
    <Field name={`region-${variant}`}>
      <Combobox items={REGIONS} multiple>
        <FieldLabel
          id={labelId}
          size={variant === "pill" ? "md" : "lg"}
          inset={false}
          htmlFor={triggerId}
        >
          {variant}
        </FieldLabel>
        {variant === "pill" ? (
          <ComboboxTrigger id={triggerId} size="md" variant="pill" placeholder="Select label" />
        ) : (
          <ComboboxTrigger id={triggerId} size="lg" variant={variant} placeholder="Select label" />
        )}
        <ComboboxContent
          aria-labelledby={labelId}
          sizing={variant === "pill" ? "auto" : undefined}
          search={<ComboboxSearch aria-label="Search" />}
        >
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} selection="checkbox" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

export default function VariantsDemo() {
  return (
    <div className="flex flex-wrap items-end gap-3">
      {VARIANTS.map((variant) => (
        <VariantField key={variant} variant={variant} />
      ))}
    </div>
  );
}

Trigger width

Field-style ComboboxTrigger controls use width="auto" by default, preserving the existing 288px minimum while allowing the trigger to grow. Use width="fit" with secondary or ghost when the closed trigger should hug its icon, selected value, trailing content, chevron, gaps, and padding. The popup keeps its own sizing behavior and can grow wider than the compact trigger. The pill variant is already intrinsically sized and does not accept width.

Show code
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxSearch,
  ComboboxTrigger,
  type ComboboxTriggerWidth,
} from "@makeplane/propel/components/combobox";
import { Field } from "@makeplane/propel/components/field";
import { Icon } from "@makeplane/propel/components/icon";
import { FieldLabel } from "@makeplane/propel/elements/field";
import { AddOutline } from "@makeplane/propel/icons";

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) => {
        const labelId = `combobox-${width}-label`;
        const triggerId = `combobox-${width}-trigger`;
        return (
          <Field key={width} name={`region-${width}`}>
            <Combobox items={REGIONS} defaultValue="us-east-1">
              <FieldLabel id={labelId} size="lg" inset={false} htmlFor={triggerId}>
                {width}
              </FieldLabel>
              <ComboboxTrigger
                id={triggerId}
                size="lg"
                width={width}
                icon={<Icon icon={AddOutline} tint="placeholder" />}
              />
              <ComboboxContent
                aria-labelledby={labelId}
                search={<ComboboxSearch aria-label="Search" />}
              >
                <ComboboxEmpty>No matches</ComboboxEmpty>
                <ComboboxList>
                  {(region: string) => <ComboboxItem key={region} value={region} label={region} />}
                </ComboboxList>
              </ComboboxContent>
            </Combobox>
          </Field>
        );
      })}
    </div>
  );
}

Chips in the field

Default is already multi-select (closed trigger + overlay). Pass multiple with ComboboxChips when the filter input lives in the field: each selected value becomes a removable chip ahead of the inline input. Option rows use selection="checkbox" so they match the dropdown checkbox items.

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 { CloseOutline } from "@makeplane/propel/icons";

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 size="lg" inset={false}>
          Regions
        </FieldLabel>
        <ComboboxChips size="lg" placeholder="Add a region">
          {(region: string) => (
            <ComboboxChip
              key={region}
              label={region}
              remove={
                <IconButton
                  variant="ghost"
                  size="xs"
                  aria-label={`Remove ${region}`}
                  icon={<Icon icon={CloseOutline} />}
                />
              }
            />
          )}
        </ComboboxChips>
        <ComboboxContent>
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} selection="checkbox" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

Limiting visible chips

Chips wrap so the inline input stays usable at narrow widths. When a selection can grow large, maxVisible renders only the first N chips plus a “+N more” count (maxVisible={0} shows N selected); pass overflowLabel to localize it. Hidden values stay selected and are managed from the popup. Arrow keys and Backspace reach only rendered chips: Backspace removes the last rendered chip, or the last selected value when none are rendered.

The elements-tier layout="collapse" single-row frame is for custom compositions.

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 { CloseOutline } from "@makeplane/propel/icons";

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

export default function MaxVisibleDemo() {
  return (
    <Field name="regions">
      <Combobox multiple items={REGIONS} defaultValue={REGIONS}>
        <FieldLabel size="lg" inset={false}>
          Regions
        </FieldLabel>
        <ComboboxChips size="lg" placeholder="Add a region" maxVisible={2}>
          {(region: string) => (
            <ComboboxChip
              key={region}
              label={region}
              remove={
                <IconButton
                  variant="ghost"
                  size="xs"
                  aria-label={`Remove ${region}`}
                  icon={<Icon icon={CloseOutline} />}
                />
              }
            />
          )}
        </ComboboxChips>
        <ComboboxContent>
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} selection="checkbox" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

ComboboxSubmenu nests a ComboboxSubmenuContent — a combobox in its own right, with its own search and items — behind a ComboboxSubmenuTrigger row that selects nothing. The row goes in the trigger prop rather than in children, because it belongs to the parent list. Point the nested root’s value / onValueChange at the parent’s state when a nested pick should become the parent’s selection, as below, or at state of its own when it is a separate 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 size="lg" inset={false}>
          Region
        </FieldLabel>
        <ComboboxInputGroup size="lg" 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} 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. useFilter and useFilteredItems are re-exported from the same module for custom filter implementations.

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 { CloseOutline } from "@makeplane/propel/icons";
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 size="lg" inset={false}>
          Reviewer
        </FieldLabel>
        <ComboboxInputGroup
          size="lg"
          placeholder="e.g. Priya"
          clear={
            <IconButton
              variant="ghost"
              size="md"
              aria-label="Clear reviewer"
              icon={<Icon icon={CloseOutline} />}
            />
          }
        />
        <ComboboxContent>
          <ComboboxStatus>{status}</ComboboxStatus>
          <ComboboxEmpty>{!pending && trimmed !== "" ? "No members match" : null}</ComboboxEmpty>
          <ComboboxList>
            {(member: string) => <ComboboxItem key={member} value={member} label={member} />}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

Sizes

size steps the trigger’s height, text, and glyph scale: md, lg, xl, or 2xl. variant="ghost" drops the bordered chrome; invalid on ghost has no trigger-level change (the danger cue is the helper text).

ComboboxContent opens inside the same overlay panel MenuContent uses. Pin a ComboboxSearch in that panel with search={<ComboboxSearch />} — that search row is the combobox input, so don’t also mount ComboboxInputGroup / ComboboxChips. Name the dialog with aria-labelledby pointing at the visible field label (or aria-label). The popup defaults to sizing="anchor" — the input’s width, grown only by its content — while sizing="auto" sizes it by its rows between the 204px floor and the 400px ceiling, and sizing="anchor-fixed" keeps an exact anchor width for windowed lists (used by ComboboxField); see Sizing.

Show code
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxSearch,
  ComboboxTrigger,
} from "@makeplane/propel/components/combobox";
import { Field } from "@makeplane/propel/components/field";
import { FieldLabel } from "@makeplane/propel/elements/field";
import { useId } from "react";

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

const SIZES = ["md", "lg", "xl", "2xl"] as const;

function SizeField({ size }: { size: (typeof SIZES)[number] }) {
  const labelId = useId();
  const triggerId = useId();
  return (
    <Field name={`region-${size}`}>
      <Combobox items={REGIONS} multiple>
        <FieldLabel id={labelId} size="lg" inset={false} htmlFor={triggerId}>
          {size}
        </FieldLabel>
        <ComboboxTrigger id={triggerId} size={size} placeholder="Select label" />
        <ComboboxContent aria-labelledby={labelId} search={<ComboboxSearch aria-label="Search" />}>
          <ComboboxEmpty>No matches</ComboboxEmpty>
          <ComboboxList>
            {(region: string) => (
              <ComboboxItem key={region} value={region} selection="checkbox" label={region} />
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}

export default function SizesDemo() {
  return (
    <div className="flex flex-wrap items-end gap-3">
      {SIZES.map((size) => (
        <SizeField key={size} size={size} />
      ))}
    </div>
  );
}

API Reference

Combobox

The combobox Root — Base UI's context/state provider (renders no element of its own). A behavior-only role, so it lives in components (rules 1a, 2); the styled parts live in elements/combobox and are grafted onto Base UI behavior here. Modal by default, unlike Base UI's root. ComboboxSubmenu roots stay non-modal — a nested backdrop would cover the parent list.

PropTypeDefaultDescription
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.
isItemEqualToValue((itemValue: Value, value: Value) => boolean)—Custom comparison logic used to determine if a combobox item value matches the current selected value. Useful when item values are objects without matching referentially. With a createItems() collection, both arguments are derived values. Defaults to Object.is comparison.
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.
multiplebooleanfalseWhether multiple items can be selected.
defaultValueComboboxValueType<Value, Multiple> | null—The uncontrolled selected value of the combobox when it's initially rendered. To render a controlled combobox, use the value prop instead.
valueComboboxValueType<Value, Multiple> | null—The selected value of the combobox. Use when controlled.
onValueChange((value: ComboboxValueType<Value, Multiple> | (Multiple extends true ? never : null), eventDetails: ChangeEventDetails) => void)—Event handler called when the selected value of the combobox changes.
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.

ComboboxTrigger

The ready-made closed-field trigger: grafts Base UI's Trigger onto the styled frame and shows the selected value (or placeholder) plus a trailing chevron. The pill variant, like SelectTrigger's, has no automatic chevron: its open state shows as active chrome. Use this instead of ComboboxInputGroup when the filter input lives in the panel (ComboboxSearch).

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.
placeholderReactNode—Shown in the trigger while nothing is selected — Base UI's Combobox.Value placeholder.
childrenReactNode—Non-interactive selected-value preview. Omit to use Base UI's labels or placeholder.
overflowReactNode—Non-interactive overflow summary, kept visible when the value preview truncates.
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.
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.

ComboboxInputGroup

The ready-made single-select input frame: grafts Base UI's InputGroup onto the styled frame and Base UI's Input onto the styled text field, laying out an optional leading icon slot and the consumer-provided clear/trigger controls. clear stays keepMounted in a reserved slot so the trailing trigger does not shift when the value is empty. When trigger is omitted, a decorative chevron is shown. All remaining props pass through to the input (the element that carries the combobox behavior and accessible name).

PropTypeDefaultDescription
size(required)"2xl" | "md" | "lg" | "xl"—Visual size of the input row: height, text, and glyph sizing. Required.
placeholderstring—Input placeholder.
variant"secondary" | "ghost""secondary"Frame look. secondary is the bordered field surface; ghost is borderless.
iconReactNode—Decorative leading element at the inline-start, e.g. <Icon icon={Search} tint="placeholder" />.
clearReactElement<unknown, string | JSXElementConstructor<any>>—The 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>>—The 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. Omit it to show a decorative chevron (the Figma end node) — the input still opens the list.

ComboboxChips

The ready-made multiselect input frame — it replaces ComboboxInputGroup in the multiple anatomy. Grafts Base UI's Chips behavior onto the styled elements/combobox frame, owning only the frame, the selected-values loop, and maxVisible overflow ahead of the inline input — each chip's template is the consumer's children, mirroring ComboboxList. Chips wrap onto new rows so the input stays usable. With maxVisible, the first N chips render followed by an overflow count (+N more, or N selected when maxVisible={0}; pass overflowLabel to localize). Arrow keys navigate rendered chips; Backspace removes the last rendered chip, or the last selected value when none are rendered.

PropTypeDefaultDescription
size(required)"2xl" | "md" | "lg" | "xl"—Visual size of the chips frame: height, text, and glyph sizing. Required.
children(required)(value: Value) => ReactNode—Renders one selected value as a ComboboxChip — the ComboboxChips counterpart of ComboboxList's item template. Set your own key, exactly like ComboboxList's function child, e.g. (value) => <ComboboxChip key={value} label={value} remove={<... />} />.
variant"secondary" | "ghost""secondary"Frame look. secondary is the bordered field surface; ghost is borderless.
placeholderstring—Input placeholder shown while typing to add another value.
maxVisiblenumberInfinityCap on how many chips render, followed by an overflow count — set it when selections can grow large. Hidden values stay selected and are managed from the popup, but Backspace and arrow keys only reach rendered chips. A nonnegative integer; zero shows only the count.
overflowLabel((hiddenCount: number, details: { visibleCount: number; }) => ReactNode)(hiddenCount, { visibleCount }) => visibleCount > 0 ? `+${hiddenCount} more` : `${hiddenCount} selected`Copy for the overflow count when maxVisible hides selected chips. Called with how many values are hidden and how many chips remain visible. Override to localize.
triggerReactElement<unknown, string | JSXElementConstructor<any>>—The popup-trigger control (e.g. an IconButton), grafted onto Base UI's Trigger behavior. It carries its own — localizable — aria-label. Omit it to show a decorative chevron (the Figma end node) — the input still opens the list.

ComboboxChip

Ready-made chip: Base UI's chip behavior (arrow-key focus, Backspace/Delete removal) grafted onto the styled tag, laying out an optional leading/trailing node around the label plus the consumer-provided remove control. Base UI derives which selected value a chip represents from its position among its siblings — pass one per value from ComboboxChips' children.

PropTypeDefaultDescription
label(required)string—The chip's label. Also sets the chip's own accessible name.
remove(required)ReactElement<unknown, string | JSXElementConstructor<any>>—The remove control, grafted onto Base UI's ChipRemove behavior. It carries its own — localizable — aria-label; the chip bakes no label or glyph.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxChipState>—Allows 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.
size"xs" | "sm" | "md" | "lg"—Chip height, text and glyph size. Inherited from the enclosing ComboboxChips when omitted.
startContentReactNode—Leading element before the label — an <Icon icon={...} />, an assignee Avatar, or similar.
endContentReactNode—Trailing element after the label, before the remove control, e.g. a status <Icon .../>.

ComboboxContent

The combobox list surface: Base UI portal + positioner, with Combobox.Popup grafted onto the same elevated OverlayPanel MenuContent uses. Search/footer therefore live _inside_ the dialog (unlike Menu, where they must sit outside role="menu"). The inner ComboboxPopup is padding-only; sticky chrome stays pinned outside the scroll area. When the input is in the panel (ComboboxSearch in search), the overlay is a dialog — name it with aria-labelledby on the visible field label, or aria-label. Filter-in-field compositions (ComboboxInputGroup / ComboboxChips) keep role="presentation" and need no dialog name.

PropTypeDefaultDescription
footerReactNode—Sticky chrome pinned below the list.
searchReactNode—Sticky chrome pinned above the list, e.g. a ComboboxSearch. When the input lives in this slot, the overlay is a role="dialog" and needs a name — pass aria-labelledby pointing at the visible field label, or aria-label.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the list along the anchor's edge without changing align.
collisionPaddingPadding16Minimum gap in px between the list and the viewport edge it would otherwise touch.
collisionBoundaryBoundary—The element or rect the list is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidance—What to do when the list would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset.
stickybooleanfalseKeep the list on screen after the anchor scrolls out of view, instead of following it out.
positionMethod"absolute" | "fixed""absolute"Which CSS position the positioner uses. fixed escapes overflow-clipping ancestors, e.g. an input inside a sticky header.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | null—Position against this element instead of the default anchor — an element, a ref, a virtual element, or a function returning one. Geometry only; the anchor keeps its role and interactions.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
viewportRefRef<HTMLDivElement>—Ref to the scrolling viewport, for virtualized lists or scroll restoration.
side"top" | "right" | "bottom" | "left" | "inline-end" | "inline-start"bottomWhich side of the input the list opens toward.
sideOffsetnumber | OffsetFunction4Distance in px between the input and the list.
align"center" | "start" | "end"startAlignment of the list relative to the input along side.
sizing"menu" | "anchor" | "auto" | "anchor-fixed" | "auto-fixed" | "sm" | "md" | "lg"anchorHow the popup takes its width. anchor matches the input's width, grows only with its content up to the room beside the input. auto is the content's width instead — never narrower than 204px, never wider than 400px or the room beside the input, and held while open so a filtering list does not reflow. anchor-fixed keeps the exact anchor width for windowed lists and caps height at 288px so a mid-open available-height settle cannot shrink under the virtualizer. auto-fixed is auto's width with the same 288px cap, for a windowed list under a compact trigger.

ComboboxSearch

A sticky search input pinned above a ComboboxContent list. Grafts Base UI's Input onto the same search-row chrome MenuSearch uses, so the combobox panel matches the dropdown. This _is_ the combobox's Input — compose it via ComboboxContent's search slot instead of ComboboxInputGroup / ComboboxChips, which already mount an input in the field. Defaults to aria-label="Search" (or the placeholder, if you changed that) so the combobox stays named.

PropTypeDefaultDescription
placeholderstringSearchPlaceholder text.
aria-labelstring"Search"Defines a string value that labels the current element. Accessible name for the search combobox. Placeholder is not a name — override to localize, or pass aria-labelledby instead. @see aria-labelledby.

ComboboxSelectAll

Sticky "select every item" row for a multi-select combobox panel. Not a ComboboxItem — it is not a list option — so compose it in ComboboxContent's search slot above the scrolling list.

PropTypeDefaultDescription
checked(required)boolean—Whether every item is currently selected.
onClick(required)() => void—Called when the row is activated.
labelstringSelect allVisible row label.
indeterminatebooleanfalseWhether only some (not all) items are currently selected — shows a dash instead of a check. Takes precedence over checked for both the glyph and aria-checked="mixed".
disabledboolean—Disables the row.

ComboboxStickyList

Pads sticky header rows (Select all + separator) with the same inset the scrolling list uses.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>—Allows 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.

ComboboxSeparator

A thin divider between groups of items in a combobox list — the same chrome as MenuSeparator. Base UI's combobox has no Separator primitive, so this is the styled element composed into the list.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, {}>—Allows 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.

ComboboxList

The combobox items container — Base UI's List behavior part (it carries no propel styling of its own), passed through so a full combobox composes without importing @base-ui/react. Inside ComboboxContent's overlay, this is the role="listbox" node; it defaults to aria-label="Suggestions" so the listbox stays named (pass aria-label / aria-labelledby to override or localize).

PropTypeDefaultDescription
aria-labelstringSuggestionsDefines a string value that labels the current element. Accessible name for the listbox. Override to localize, or pass aria-labelledby instead. @see aria-labelledby.
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxListState>—Allows 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.

ComboboxItem

The ready-made option row: Base UI's Item behavior grafted onto the shared menu-row chrome so the combobox list matches the dropdown panel. selection="check" reserves a TRAILING check gutter (kept mounted while unselected); selection="checkbox" shows the LEADING checkbox box used by multi-select.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxItemState>—Allows 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.
variant"neutral" | "accent" | "danger""neutral"Row look. neutral is the standard text hierarchy, accent marks the primary action, and danger marks a destructive one.
selection"checkbox" | "check"checkSelection marker. check is the trailing accent check used by single-select; checkbox is the leading checkbox box used by multi-select (the Figma dropdown row).
iconReactNode—Leading element shown before the label, e.g. <Icon icon={MapPin} tint="secondary" />. On a selection="checkbox" row it follows the checkbox box.
trailingReactNode—Content pinned at the row's inline end, e.g. a count.
valueunknown—Option value whose string form is used when label is omitted. Option value. Object values cannot derive display text, so they require label.
labelstring—Option label. Defaults to the string form of value. Visible option label. Required when value is not a primitive.

ComboboxSubmenu

One nested combobox: the parent row that opens it, plus its own Base UI Combobox.Root. The row itself carries no value — it is a way into the nested list, and only that list's items are selectable — so the two roots keep separate values. Point this root's value/onValueChange at the parent's state when the nested picks should land in the parent's selection, or at state of its own when they should not. Base UI has no combobox submenu primitive (only Menu does), so the nesting is assembled here. The nested popup still portals to <body>, but it stays a React-tree descendant of the parent popup, which is what stops a press inside it from reading as an outside-press that dismisses the parent. Selecting a nested item closes the nested list (Base UI's own single-select behavior) and leaves the parent list open. Closing the parent too is the consumer's call — do it from onValueChange, where the parent's own open is in reach.

PropTypeDefaultDescription
trigger(required)ReactNode—The parent row that opens this nested list — a ComboboxSubmenuTrigger. It is a **prop**, not a child, because it belongs to the PARENT list: the row is one of that combobox's options (so it stays arrow-key navigable and filterable there) while everything under children belongs to the nested combobox this part roots. Rendering it here keeps it above that nested root rather than inside it.
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.
isItemEqualToValue((itemValue: Value, value: Value) => boolean)—Custom comparison logic used to determine if a combobox item value matches the current selected value. Useful when item values are objects without matching referentially. With a createItems() collection, both arguments are derived values. Defaults to Object.is comparison.
actionsRefRefObject<Actions | null>—A ref to imperative actions. - unmount: Manually unmounts the combobox. Call this after any externally controlled closing animation finishes.
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.
multiplebooleanfalseWhether multiple items can be selected.
defaultValueComboboxValueType<Value, Multiple> | null—The uncontrolled selected value of the combobox when it's initially rendered. To render a controlled combobox, use the value prop instead.
valueComboboxValueType<Value, Multiple> | null—The selected value of the combobox. Use when controlled.
onValueChange((value: ComboboxValueType<Value, Multiple> | (Multiple extends true ? never : null), eventDetails: ChangeEventDetails) => void)—Event handler called when the selected value of the combobox changes.
openboolean—Whether the nested list is open. Controlled; pair with onOpenChange.
defaultOpenbooleanfalseWhether the nested list is open on mount. Uncontrolled.
onOpenChange((open: boolean, eventDetails?: ChangeEventDetails) => void)—Called with the next open state when the nested list opens or closes. eventDetails is Base UI's own — forwarded verbatim, so reason still separates an escapeKey close from an outsidePress or a completed itemPress. It is optional here, unlike on a Base UI root, because propel drives the hover and row-press paths itself (Base UI ships no combobox submenu to drive them) and exposes no way to mint details for them: those two calls arrive with the flag alone.
openOnHoverbooleantrueOpen the nested list when the pointer rests on the parent row, the way a menu submenu does. Turn it off for lists the user sweeps while reading, where an unasked-for search field is a nuisance — the row still opens on click or on Enter while highlighted.
delaynumber100How long in ms the pointer must rest on the row before a hover-open fires.
closeDelaynumber150How long in ms the nested list stays open after the pointer leaves both it and the row. The grace period is what lets the pointer cross the gap between the two.
closeParentOnEscbooleanfalseWhether Escape inside the nested list closes the whole stack, or only this list. Mirrors MenuSubmenu's prop of the same name — and its default, which steps back out to the parent rather than dismissing everything.
childrenReactNode—The nested ComboboxSubmenuContent (and nothing else — the row goes in trigger).

ComboboxSubmenuTrigger

The parent row that opens a nested combobox. It is an option of the PARENT list — Base UI's Combobox.Item behavior, so it keeps that list's highlight, arrow-key navigation and filtering — but it carries **no value**: the pointer and Enter paths both suppress Base UI's own selection (preventBaseUIHandler) and open the nested list instead. Only the nested list's items select. Pass it to ComboboxSubmenu's trigger prop rather than rendering it as a child, so it resolves against the parent combobox's context instead of the nested root's. It still registers with the parent list as an item, so a parent that derives its options from the DOM rather than an items prop counts this row among them. Give such a parent an explicit items list when that matters (a ComboboxSelectAll, for instance, should not count it).

PropTypeDefaultDescription
label(required)string—Row label — the name of the nested list, not of a value.
variant"neutral" | "accent" | "danger""neutral"Row look. neutral is the standard text hierarchy, accent marks the primary action, and danger marks a destructive one.
iconReactNode—Leading element shown before the label, e.g. <Icon icon={Users} tint="secondary" />.
trailingReactNode—Content pinned before the submenu chevron, e.g. a count.

ComboboxSubmenuContent

The floating surface for a nested combobox — the same elevated, scrollable panel as ComboboxContent, but anchored beside its parent row instead of below the field. Place it inside a ComboboxSubmenu, which supplies both the row it anchors to and the nested root it belongs to. It has no anchor prop: the parent row is the anchor, and overriding it would leave the row highlighted next to a panel somewhere else. Same naming contract as ComboboxContent: a nested list with its own ComboboxSearch is a role="dialog" and must be named — pass aria-label (the parent row's label reads well) or aria-labelledby. Name its ComboboxList too, since that list, not the row, is what a screen reader lands in when the nested combobox opens.

PropTypeDefaultDescription
footerReactNode—Sticky chrome pinned below the list.
searchReactNode—Sticky chrome pinned above the list, e.g. a ComboboxSearch. When the input lives in this slot, the overlay is a role="dialog" and needs a name — pass aria-labelledby pointing at the visible field label, or aria-label.
alignOffsetnumber | OffsetFunction0Additional offset in px along the align axis — slides the list along the anchor's edge without changing align.
collisionPaddingPadding16Minimum gap in px between the list and the viewport edge it would otherwise touch.
collisionBoundaryBoundary—The element or rect the list is confined to when avoiding collisions. Base UI's clipping ancestors when unset.
collisionAvoidanceCollisionAvoidance—What to do when the list would overflow the boundary, set per axis: flip to the other side, shift along it, or stay put. Base UI flips the side and shifts the alignment when unset.
stickybooleanfalseKeep the list on screen after the anchor scrolls out of view, instead of following it out.
positionMethod"absolute" | "fixed""absolute"Which CSS position the positioner uses. fixed escapes overflow-clipping ancestors, e.g. an input inside a sticky header.
disableAnchorTrackingbooleanfalseStop tracking layout shifts of the anchor — position once, on open.
viewportRefRef<HTMLDivElement>—Ref to the scrolling viewport, for virtualized lists or scroll restoration.
side"top" | "right" | "bottom" | "left" | "inline-end" | "inline-start"rightWhich side of the parent row the nested list opens toward.
sideOffsetnumber | OffsetFunction4Distance in px between the parent row and the nested list.
align"center" | "start" | "end"startAlignment of the nested list relative to the parent row along side.
sizing"menu" | "anchor" | "auto" | "sm" | "md" | "lg"autoHow the nested popup takes its width. auto is the content's width — never narrower than 204px, never wider than 400px or the room beside the row, and held while open so a filtering list does not reflow. anchor matches the parent row's width instead, which is rarely what a nested list wants. anchor-fixed and auto-fixed are not accepted here; they are ComboboxContent-only.

ComboboxItemIndicator

Ready-made combobox item indicator: Base UI's selection behavior grafted onto the styled marker, with a default check when no children are given (defaults are a components concern). Composed at the row's inline-end. Mounted on every row by default, so a selection change never re-truncates a label and the reserved column holds while unselected (the styled marker hides its glyph off data-selected).

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxItemIndicatorState>—Allows 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.

ComboboxEmpty

The ready-made empty-state row: Base UI's Empty behavior grafted onto the styled elements/combobox row. Pass the — localizable — no-matches message as children.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxEmptyState>—Allows 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.

ComboboxStatus

A polite live region inside the popup for async hints ("Searching…", "12 results") — Base UI's Status behavior grafted onto the shared muted listbox hint styling.

No component-specific props — accepts the standard HTML attributes for the element it renders.

ComboboxGroup

Groups related options under a ComboboxGroupLabel. Pass the group's items so the nested ComboboxCollection renders (and filters) just this group's options.

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxGroupState>—Allows 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.

ComboboxGroupLabel

The muted heading naming a ComboboxGroup, with optional meta pinned at its end. Base UI's GroupLabel behavior is grafted onto the heading's TITLE, not the whole row, so the group's accessible name is the heading text alone — a count or a "Clear" button in meta stays out of it. Place it inside the ComboboxGroup it names, so Base UI wires it as that group's label.

PropTypeDefaultDescription
metaReactNode—Content pinned at the heading's inline end, e.g. a count or a "Clear" action.

ComboboxCollection

Renders the filtered items of the nearest ComboboxGroup (or the root) through a function child — the grouped counterpart of ComboboxList's function child.

No component-specific props — accepts the standard HTML attributes for the element it renders.

ComboboxRow

A grid row wrapper for multi-column listbox layouts (pass cols on the root's grid).

PropTypeDefaultDescription
renderReactElement<unknown, string | JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, ComboboxRowState>—Allows 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.