Multi-value dropdown picker built on Base UI Select in multiple mode. Selected values render as removable chips; the option list is fixed (no text filtering).
Reach for MultiSelect when the user picks several values from a fixed list and no text filtering is needed.
MultiCombobox.Select.Use @uxf/ui/multi-select for a controlled component outside a form. Inside a @uxf/form form, use @uxf/form/multi-select instead — it registers the field with react-hook-form and derives isInvalid / helperText from validation. This @uxf/ui component is the controlled primitive the form twin wraps.
import { MultiSelect, MultiSelectOption, MultiSelectValue } from "@uxf/ui/multi-select";
import { useState } from "react";
const options: MultiSelectOption<string>[] = [
{ color: "red", id: "one", label: "Option red" },
{ color: "blue", id: "two", label: "Option blue" },
{ disabled: true, id: "three", label: "Option three" },
];
function Example() {
const [value, setValue] = useState<MultiSelectValue<string>>(null);
return (
<MultiSelect
label="Choose options"
name="example"
onChange={setValue}
options={options}
placeholder="Select..."
value={value}
/>
);
}
value is an array of ids (MultiSelectValue<T> = T[] | null) — not option objects — and onChange receives the new array of ids, always in canonical (sorted) order rather than in click order (see Value order). Selected chips can be removed with their close button or by pressing Backspace.
Exports: MultiSelect and the types MultiSelectProps, MultiSelectOption, MultiSelectValue, MultiSelectValueId (deprecated alias of SelectableId), MultiSelectTypeRef.
MultiSelectProps<ValueId = SelectableId, Option = MultiSelectOption<ValueId>> extends FormControlProps<ValueId[] | null>. SelectableId is string | number (from @uxf/core/types); MultiSelectOption is { id: ValueId; label: ReactNode; color?: ChipColor; disabled?: boolean }. It is not Clearable — there is no isClearable prop.
| Prop | Type | Default | Description |
|---|---|---|---|
label | ReactNode | — | Required. Field label. |
name | string | — | Required. Field name (also set as data-name). |
value | ValueId[] | null | — | Required. Selected option ids, or null. |
onChange | (value: ValueId[]) => void | — | Required. Called with the new array of selected ids, in canonical (sorted) order. |
options | Option[] | — | Required. Selectable options. |
withCheckboxes | boolean | false | Render options as a checkbox list (keeps selected options visible in the list). |
placeholder | string | — | Shown when nothing is selected. |
helperText | ReactNode | — | Text under the control; styled as an error when isInvalid. |
isDisabled | boolean | false | Disable the control. |
isReadOnly | boolean | false | Prevent changes while staying focusable. |
isInvalid | boolean | false | Invalid styling; sets aria-invalid and styles helperText as an error. |
isRequired | boolean | false | Mark the field required (adds the label indicator). |
hiddenLabel | boolean | false | Visually hide the label (kept for assistive tech). |
renderOption | (option: Option) => ReactNode | option.label | Custom renderer for each dropdown option (ignored when withCheckboxes). |
keyExtractor | (option: Option) => string | number | option.id | Custom React key for options. |
noOptionsMessage | string | localized "No items found" | Empty-list message. |
allOptionsSelectedMessage | string | localized "All options are already selected" | Shown when every option is selected. |
iconName | IconName | "caretDown" | Icon for the dropdown arrow. |
size | InputGroupSize | "default" | "small", "default", or "large". |
variant | InputGroupVariant | "default" | Input group variant. |
id | string | auto (useId) | Root id; the error-message id derives from it. |
onBlur / onFocus | FocusEventHandler<HTMLInputElement> | — | Focus handlers. |
The component is a forwardRef — the ref points at the trigger <div> (MultiSelectTypeRef = HTMLDivElement). Layout/dropdown props: className, style, form, leftAddon, rightAddon, leftElement, rightElement, inputArrow, dropdownClassName, dropdownMatchesInputWidth, dropdownMaxHeight (default 240), dropdownPlacement (default "bottom"), dropdownStrategy.
The value is semantically a set, and the emitted array is always sorted by id
(normalizeSelectableIds) rather than kept in click order.
Rendering is unaffected — the chips render from the options-ordered selection, never from the value order.
This matters for react-hook-form: it compares arrays index by index, so a click-ordered value made a form
read as dirty after an option was deselected and selected again. The component can only canonicalize what
it emits, so run your defaultValues through the same helper:
const formApi = useForm<FormData>({ defaultValues: { tags: normalizeSelectableIds(data.tagIds) } });
If you submit this value somewhere order-sensitive, note that the payload order changed.
withCheckboxes renders each option as a checkbox and keeps selected options in the list rather than hiding them.Chip; its color comes from the option's color. Chips are removable unless the control or the option is disabled.small, default, large (via size).isInvalid, isDisabled, isReadOnly).The component composes the input, label, dropdown, form-component, icon, and chip primitives, so import their stylesheets once in your global CSS:
@import url("@uxf/ui/css/icon.css");
@import url("@uxf/ui/css/dropdown.css");
@import url("@uxf/ui/css/label.css");
@import url("@uxf/ui/css/form-component.css");
@import url("@uxf/ui/css/input-basic.css");
@import url("@uxf/ui/css/input.css");
@import url("@uxf/ui/css/chip.css");
@import url("@uxf/ui/css/multi-select.css");
When using withCheckboxes, also import the checkbox stylesheets:
@import url("@uxf/ui/css/checkbox.css");
@import url("@uxf/ui/css/checkbox-input.css");
Also requires the global @uxf/ui token layer, set up once per app — see @uxf/ui setup.