Quire UI
Controls13

Multi-select

Pick several from a list you search. Results can load as you type, a pasted list adds every match, and the field keeps one line at rest.

Try pasting “design, docs, backend”.

Installation

npx shadcn@latest add @quireui/multi-select

Usage

const [labels, setLabels] = useState<string[]>([]);

<MultiSelect
  aria-label="Labels"
  placeholder="Add labels"
  options={[
    { value: "bug", label: "Bug" },
    { value: "feature", label: "Feature" },
  ]}
  value={labels}
  onValueChange={setLabels}
/>

The value is the list of chosen values, like ["bug", "feature"]. onValueChange also gets the options they stand for, for when you need their labels.

Options can carry an icon (an avatar or a colour dot), a description on a second line, and a group to sit under a heading. The part of each row that matches what you typed is set in bold.

One line at rest

At rest the field keeps the height of every other input: the chips that fit show, and the rest fold into "+3". While you edit, it opens up to show every chip and grows over what is below it, so the form around it never moves. When you leave, it folds back.

Chosen first

When the list opens, what was already chosen sits on top under "Selected", so a long selection is easy to check and trim. Picks made while it is open stay where they are, so nothing moves under the pointer. Picking while searching clears the search and keeps the list open for the next one.

Loading as you type

For lists too long to send at once, pass load. It is called with what was typed after a short pause, and its signal cancels requests that are no longer needed. Results are cached by search, and the last ones stay on screen while the next ones load, so the list does not flicker. A failed load says so, with Retry.

<MultiSelect
  options={team.slice(0, 5)}
  load={async (query, { signal }) => {
    const response = await fetch(`/api/people?q=${encodeURIComponent(query)}`, { signal });
    return response.json();
  }}
  value={reviewers}
  onValueChange={setReviewers}
/>

Chosen values keep their labels when they are not among the current results. Pass the ones it starts with in options, and every option a load returns is remembered.

Pasting a list

Paste "ada@acme.co, lena@acme.co" or a column copied from a spreadsheet, and every match is added at once: by label or by value, ignoring case, and through load when it is not on screen. The list then says what happened, "Added 2 · Not found: tim@acme.co".

Creating

With onCreate, typing something that does not exist offers "Create “Perf”". It is called with the text and returns the new option, at once or as a promise; a spinner shows while it saves, and the new option is chosen. Pasted words that match nothing are created too.

Limits

max caps how many can be chosen. Once it is reached, the rest of the list fades and the list says "Up to 6".

Forms

With name, each chosen value is sent with the form as a repeated field, so formData.getAll(name) reads them all.

Accessibility

The field is a combobox with its list, and each row is an option marked as selected or not. Each chip has a remove button named after it, "Remove Bug". Loading, errors and what a paste did are announced politely.

KeyAction
TypingSearches, and highlights the first match
↓ / ↑Opens the list, then moves the highlight
EnterChooses or unchooses the highlighted row; the list stays open
BackspaceIn the empty field: removes the last chip
← / →In the empty field: moves between chips; Backspace removes the one in focus
EscCloses the list. It never clears the chips

Motion

The list opens with the library's popover motion. The field opens up and folds back instantly, and rows are highlighted without easing, so the highlight keeps up with the pointer and the keys.

Multi-select or something else?

  • A short list where every choice fits on screen: a group of Checkboxes.
  • A few chips from a fixed list, with nothing to load: Combobox with multiple.

API reference

MultiSelect

Picks several things from a list you search, like people or labels. Results can load as you type; what was chosen opens pinned on top; a pasted list adds every match at once; and the field keeps one line at rest, folding what does not fit into "+3".

Prop

Type

Style it with [data-slot="multi-select"].

On this page