EntityMultiPicker
Searches one or more entity types on the server as you type, and binds the rows you tick.
Single and multi: EntityPicker binds one row.
Install
Section titled “Install”pnpm dlx shadcn@latest add https://sg-widgets.vercel.app/r/react/entity-multi-picker.jsonpnpm dlx shadcn-svelte@latest add https://sg-widgets.vercel.app/r/svelte/entity-multi-picker.jsonReact
Loading the React demo…
Svelte
Loading the Svelte demo…
| prop | type | default | meaning |
|---|---|---|---|
summary | 'chips' | 'ellipsis' | 'count' | 'ellipsis' | What the control shows for the selection. |
max | number | 0 | Chips drawn before the rest becomes +n. 0 lets the row fit what it can. |
entityTypesfrom EntityPicker | string[] | required | Types to search. Several make the picker polymorphic. |
contextfrom EntityPicker | SgContext | required | The widget context. Every read goes through it, so widgets on a page share one cache. |
value | EntityRef[] | [] | The chosen rows, in the order they were ticked. Two-way in Svelte. |
labelFieldfrom EntityPicker | string | display-name chain | Field holding the row label. |
searchFieldsfrom EntityPicker | SearchFieldSpec[] | ((query) => SearchFieldSpec[]) | — | Fields matched on top of the display-name chain. A function is called with the query. |
secondaryFieldfrom EntityPicker | string | CollectionColumn | null | null | Field 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 EntityPicker | string | CollectionColumn | null | null | Field shown under the label, drawn by its data type: a status reads its display name. |
subLabelfrom EntityPicker | (row) => string | the type, when several are searched | Computes the sub-label. |
thumbnailfrom EntityPicker | string | false | 'image' | Field holding the thumbnail URL. false hides the leading slot. |
roundThumbnailfrom EntityPicker | boolean | false | Draws the thumbnail as a circle. |
showCodefrom EntityPicker | boolean | false | Shows the row's code beside the label when the two differ. |
siteUrlfrom EntityPicker | string | the context's | The site the status sprite is served from, for a secondary that is a status. |
fieldsfrom EntityPicker | string[] | — | Extra fields to request. |
filtersfrom EntityPicker | FilterGroup | WireGroup | null | null | Pre-filter, merged into every search with and. |
projectIdfrom EntityPicker | number | — | Scopes to one project. |
exclude | EntityRef[] | — | Rows kept out of the results, per type. |
minQueryLengthfrom EntityPicker | number | 0 | Under it the query carries no name condition. |
pageSizefrom EntityPicker | number | 20 | Rows a page, with a load more row under them. |
debounceMsfrom EntityPicker | number | 250 | Wait after the last keystroke before searching. |
class / classNamefrom EntityPicker | string | — | Merged after the widget's own classes. |
sizefrom PickerControl | 'sm' | 'md' | 'lg' | 'md' | Control height: 7, 8 and 9. |
disabledfrom PickerControl | boolean | false | |
readonlyfrom EntityPicker | boolean | false | Keeps full contrast and drops the affordances. |
invalidfrom EntityPicker | boolean | false | Sets aria-invalid and the destructive ring. |
clearable | boolean | true | Shows the clear-all control. |
placeholderfrom EntityPicker | string | 'Search for an entity' | Shown in the control while nothing is chosen. |
searchPlaceholderfrom EntityPicker | string | 'Search…' | Shown in the query input once something is chosen. |
openfrom PickerControl | boolean | required | React: whether the popup is showing. |
openfrom PickerControl | boolean | false | Svelte: whether the popup is showing, two-way. |
emptyLabelfrom EntityPicker | string | 'No match' | Shown when a query matches nothing. |
loadingLabelfrom EntityPicker | string | 'Loading…' | Names the skeletons a read stands behind, for a screen reader. |
errorLabelfrom EntityPicker | string | — | Shown in place of what the failed read said. |
The secondary column shows nothing until secondaryField or secondary asks for something, and
secondaryField is drawn by the field’s data type: a status is a badge, a date is formatted.
Selected rows are appended to the option list after the search results, so a row stays there to be unticked whatever the query. Each selection is also a chip in the control, with its own remove control.
chips makes the control a token field: every chip, wrapped, with the caret beside them, and
+n after the last chip max allows.
ellipsis makes it a one-line trigger: as many whole chips as the row fits, never a cut one,
then +n inline after the last, the whole list in the tooltip, and the search box at the top of
the popup.
count makes it the same trigger reading 3 selected.
A press on +n opens the list, where the hidden rows are.
Events
Section titled “Events”| event | payload | when |
|---|---|---|
onValueChange | EntityRef[], PickerRow[] | The selection changed. |
onErrorfrom EntityPicker | Error | A read failed. The list shows it inline as well. |
onOpenChangefrom EntityPicker | boolean | The popup opened or closed. Svelte also binds it with bind:open. |
None. The row is fixed: checkbox, leading thumbnail or avatar, label with the matched words in bold, sub-label, right-aligned secondary.
Keyboard
Section titled “Keyboard”| key | does |
|---|---|
| any text | Searches the server, from the caret under chips and from the popup's search box otherwise. |
Enter, Spacefrom EntityPicker | On the chevron, opens the list. |
Up, Downfrom EntityPicker | Moves 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 PickerControl | Moves the highlight, and the list scrolls it into view. |
Enter | Ticks or unticks the highlighted row. The list stays open. |
Escapefrom EntityPicker | Closes the list and clears the query. On a closed picker it does nothing. |
Backspace | In an empty search box, highlights the last chip. A second one removes it. Any other key releases it. |
ArrowLeft / ArrowRightfrom PickerControl | On a chip, walks the row. ArrowRight past the last chip returns to the input. |
Deletefrom PickerControl | On a chip, removes it and leaves the caret on its neighbour. |
Tab | Moves through the chips' remove controls, then +n, the clear control and the chevron. |
API behaviour
Section titled “API behaviour”The same request model as the single picker: one contains condition per word, or’d across the
display-name fields the type has, one POST /entity/<type>/_search per searched type, and
client-side filtering off (017_filter_operators, 030_complex_filters). With nothing typed the name
condition is dropped and the page is sorted -updated_at (026_result_order).
Exclusions go into the server filter as id not_in per type, so they never eat into the page. The
legacy behaviour of filtering them out after the page cap could empty a dropdown that had matches.
Bare { type, id } members are resolved on mount by one id in read per type, batched, with the
page size set to the number of ids. A resolved row is never overwritten by the bare reference it
came from.
Reference
Section titled “Reference”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.