Skip to content

UserMultiPicker

Searches people, and optionally script accounts, on the server, and binds the rows you tick.

Single and multi: UserPicker takes one person.

Terminal window
pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/user-multi-picker.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/user-multi-picker.json
UserMultiPicker: chips, scripts, inactive people, hydration, summary modes, sizes, states

React

Loading the React demo…

Svelte

Loading the Svelte demo…

proptypedefaultmeaning
includeApiUsersbooleantrueSearch script accounts alongside people.
includeInactivebooleanfalseOffer people whose status is dis.
summaryfrom EntityMultiPicker'chips' | 'ellipsis' | 'count''ellipsis'What the control shows for the selection.
maxfrom EntityMultiPickernumber0Chips drawn before the rest becomes +n. 0 lets the row fit what it can.
contextfrom EntityPickerSgContextrequiredThe widget context. Every read goes through it, so widgets on a page share one cache.
valueEntityRef[][]The chosen people, in the order they were ticked. Two-way in Svelte.
labelFieldfrom EntityPickerstringdisplay-name chainField holding the row label.
searchFieldsfrom EntityPickerSearchFieldSpec[] | ((query) => SearchFieldSpec[])—Fields matched on top of the display-name chain. A function is called with the query.
secondaryFieldfrom EntityPickerstring | CollectionColumn | nullnullField shown right-aligned, drawn by its data type. A resolved column renders it by type.
secondaryfrom EntityPicker(row) => string—Right-aligned text of your own. Wins over secondaryField.
subLabelFieldfrom EntityPickerstring | CollectionColumn | nullnullField shown under the label, drawn by its data type: a status reads its display name.
subLabelfrom EntityPicker(row) => stringthe type, when several are searchedComputes the sub-label.
thumbnailfrom EntityPickerstring | false'image'Field holding the thumbnail URL. false hides the leading slot.
roundThumbnailfrom EntityPickerbooleanfalseDraws the thumbnail as a circle.
showCodefrom EntityPickerbooleanfalseShows the row's code beside the label when the two differ.
siteUrlfrom EntityPickerstringthe context'sThe site the status sprite is served from, for a secondary that is a status.
fieldsfrom EntityPickerstring[]—Extra fields to request.
filtersfrom EntityPickerFilterGroup | WireGroup | nullnullPre-filter, merged into every search with and.
projectIdfrom EntityPickernumber—Scopes to one project.
excludefrom EntityMultiPickerEntityRef[]—Rows kept out of the results, per type.
minQueryLengthfrom EntityPickernumber0Under it the query carries no name condition.
pageSizefrom EntityPickernumber20Rows a page, with a load more row under them.
debounceMsfrom EntityPickernumber250Wait after the last keystroke before searching.
class / classNamefrom EntityPickerstring—Merged after the widget's own classes.
sizefrom PickerControl'sm' | 'md' | 'lg''md'Control height: 7, 8 and 9.
disabledfrom PickerControlbooleanfalse
readonlyfrom EntityPickerbooleanfalseKeeps full contrast and drops the affordances.
invalidfrom EntityPickerbooleanfalseSets aria-invalid and the destructive ring.
clearablefrom EntityMultiPickerbooleantrueShows the clear-all control.
placeholderstring'Search for people'Shown in the control while nothing is chosen.
searchPlaceholderfrom EntityPickerstring'Search…'Shown in the query input once something is chosen.
openfrom PickerControlbooleanrequiredReact: whether the popup is showing.
openfrom PickerControlbooleanfalseSvelte: whether the popup is showing, two-way.
emptyLabelfrom EntityPickerstring'No match'Shown when a query matches nothing.
loadingLabelfrom EntityPickerstring'Loading…'Names the skeletons a read stands behind, for a screen reader.
errorLabelfrom EntityPickerstring—Shown in place of what the failed read said.

placeholder defaults to Search for people. summary and max set what the control shows for the selection, as on every other multi picker.

The query is matched against the display-name chain and the email, and against login while the query holds no whitespace. Both fields are requested so the row can show them. A person’s sub-label is their email; a script account’s is API user. subLabelField="login" puts the login there instead.

Every row carries a checkbox and the list stays open across selections. Selected rows are appended to the option list after the search results, so a person stays there to be unticked whatever the query. Each selection is also a chip in the control, with its own remove control.

eventpayloadwhen
onValueChangefrom EntityMultiPickerEntityRef[], PickerRow[]The selection changed.
onErrorfrom EntityPickerErrorA read failed. The list shows it inline as well.
onOpenChangefrom EntityPickerbooleanThe popup opened or closed. Svelte also binds it with bind:open.

None.

keydoes
any textfrom EntityMultiPickerSearches the server, from the caret under chips and from the popup's search box otherwise.
Enter, Spacefrom EntityPickerOn the chevron, opens the list.
Up, Downfrom EntityPickerMoves the highlight through the rows and keeps it in view, including across a load more page. The last row holds it rather than wrapping.
ArrowDown / ArrowUpfrom PickerControlMoves the highlight, and the list scrolls it into view.
Enterfrom EntityMultiPickerTicks or unticks the highlighted row. The list stays open.
Escapefrom EntityPickerCloses the list and clears the query. On a closed picker it does nothing.
Backspacefrom EntityMultiPickerIn an empty search box, highlights the last chip. A second one removes it. Any other key releases it.
ArrowLeft / ArrowRightfrom PickerControlOn a chip, walks the row. ArrowRight past the last chip returns to the input.
Deletefrom PickerControlOn a chip, removes it and leaves the caret on its neighbour.
Tabfrom EntityMultiPickerMoves through the chips' remove controls, then +n, the clear control and the chevron.

login is the only unique field on HumanUser and is what impersonation matches; email is not unique, and code does not exist at all, so a filter on it is a 400 (entity_types/HumanUser).

Everyone on a site shares an email domain, so the address is matched with starts_with until the query holds an @, and with contains after that. contains on the whole address would match the domain and with it every person on the site (017_filter_operators).

sg_status_list on HumanUser is two codes, act and dis, with act the default (entity_types/HumanUser). Active-only is that condition, and it is dropped on ApiUser, which has no status field: the same name in a filter on a type that lacks it would be a 400 (017_filter_operators).

HumanUser is site-wide and has no project field; a person’s membership is the projects multi-entity on the row, so projectId scopes through that instead (entity_types/HumanUser).

The row, chip and checkbox anatomy follows shadcn’s Base UI Combobox, read through shadcn 4.21.0. The primitive underneath is Base UI Combobox 1.8.0 in React and Bits UI Combobox 2.19.1 in Svelte. shadcn-svelte ships no combobox item, so each widget composes the headless primitive of its framework and both draw the same rows, the same classes and the same states.