Combobox
A closed-field picker that selects one or more options from a searchable overlay panel — the same shell the dropdown menu uses.
Show codeHide code
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 codeHide code
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 codeHide code
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 codeHide code
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 codeHide code
Submenu
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 codeHide code
Async search
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 codeHide code
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 codeHide code
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.
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).
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).
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.
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.
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.
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.
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.
ComboboxStickyList
Pads sticky header rows (Select all + separator) with the same inset the scrolling list uses.
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.
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).
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.
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.
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).
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.
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).
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.
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.
ComboboxGroup
Groups related options under a ComboboxGroupLabel. Pass the group's items so the nested
ComboboxCollection renders (and filters) just this group's options.
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.
ComboboxCollection
Renders the filtered items of the nearest ComboboxGroup (or the root) through a function child
— the grouped counterpart of ComboboxList's function child.
ComboboxRow
A grid row wrapper for multi-column listbox layouts (pass cols on the root's grid).