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-selectUsage
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.
| Key | Action |
|---|---|
| Typing | Searches, and highlights the first match |
| ↓ / ↑ | Opens the list, then moves the highlight |
| Enter | Chooses or unchooses the highlighted row; the list stays open |
| Backspace | In the empty field: removes the last chip |
| ← / → | In the empty field: moves between chips; Backspace removes the one in focus |
| Esc | Closes 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"].