Skip to contentAsheeUIAsheeUI

MultiSelect

Several options at once, counted in the trigger and named as removable chips.

A multi-select asks for a set rather than a value, so it reports in two places: the trigger counts what is held, and the chips below it name each choice and offer to remove it. It shares the dropdown's menu, search, keyboard navigation, disabled options, and differs by accepting a list, never closing on a choice, and keeping the selection visible after the menu is gone.

Usage

import { MultiSelect } from "@asheeui/web";

const topics = [
  { label: "Invoices", value: "invoices" },
  { label: "Payments", value: "payments" },
];

<MultiSelect
  label="Topics you follow"
  InputLabel="Choose topics..."
  options={topics}
  value={values}
  onChange={setValues}
/>

Two props say what the field is called. label is the field's label, above the control, and InputLabel is the trigger's empty text, shown while nothing is selected, it defaults to "Select options", so state it. A value array controls the selection; without one the component keeps the accumulated selection itself and still reports every change through onChange.

Choosing an option toggles it, and the menu stays open, so a set is built in one visit rather than one visit per option.

Selection and chips

The chips are derived from value, so by default they cannot disagree with the selection. chipLabel puts a caption above them, and disableChipDisplay hides the panel entirely while the trigger keeps counting, which suits a field whose chips would crowd the form. Chip appearance comes from the chip bag: color, size, radius and a className override. Its size is registered as sm, its colour falls back to the trigger's resolved colour, and its radius to the trigger's radius and then sm.

When the selection is owned elsewhere, a table's filter store, a wizard, chipOptions replaces the derived chips and handleAddChip and handleRemoveChip replace the toggle, so the component draws what it is given instead of deciding.

Search is on by default here, unlike the dropdown: isSearch adds the field at the top of the menu, searchPlaceholder and searchInputName name it, and belowList renders under the options.

Props

The trigger follows the field family's web defaults, size is md and variant is bordered, while radius follows the application's defaultRadius and color its defaultColor. A status of error outranks that colour with the semantic danger role and sets aria-invalid, which is the family's rule rather than this component's.

The chip bag resolves separately: chip.size is registered sm, chip.color falls back to the resolved trigger colour, and chip.radius to the trigger's radius and then sm. Write them under components.multiSelect.chip to change them once, or in the chip prop to change them for one instance.

Accessibility

  • The trigger is a real <button type="button"> with aria-haspopup="listbox" and aria-expanded, named by the field's label. The description and message are referenced with aria-describedby, and an error status sets aria-invalid.
  • The list is a role="listbox" carrying aria-multiselectable, and each option is a role="option" with aria-selected. Choosing one toggles it without closing the menu, so a keyboard user can build the whole set before Escape closes it and returns focus to the trigger.
  • Keyboard navigation is the menu's: ↑ and ↓ move the current option and wrap, Home and End jump to the edges, typed characters jump to the first option whose label starts with them, and the search field filters the list as you type.
  • Each chip's remove control is a real button whose accessible name is "Remove" followed by the option's label, so a chip can be removed without a pointer; while the field is disabled those controls leave the tab order with it.

The label, description and message behave as the field anatomy describes.