Skip to content

Components · Forms

EntityPicker

Pick one or several records (projects, contacts, teams) from a searchable list in a popover.

@rookcom/monarch-reactComponent

Project with groups, clear option and footer

Usage examples

Multiple with create

Chip trigger

Purpose

The "click the field, search, pick a record" pattern most products end up rebuilding: a trigger (a field-style button or the product’s own chip) opens a popover with a search box and a listbox. The product injects the records, search, rendering and creation; EntityPicker owns the trigger, popover, keyboard and selection model.

Use when

  • Picking a project, contact, company, team, status or space from a list that may be long or server-searched.
  • The value is shown as a compact chip or field and edited in place (issue properties, ticket sidebars).
  • Several records, with the list staying open while toggling (multiple).

Avoid when

  • A short fixed list with no search: use Select.
  • People: use AssigneePicker, which adds avatars, "you" and "Unassigned".
  • Typing straight into a form field: use Combobox.

Design principles

  • No product data in Monarch: records arrive as options or from onSearch, and rich rows come from media, description, hint or renderOption.
  • One picker for every record type, so keyboard behaviour and accessibility are the same everywhere.
  • Prefix matches rank first (sortByMatch, on by default), so "in" finds India before Argentina.
  • Single pick closes and returns focus to the trigger; multiple keeps the list open.

API reference

import { EntityPicker } from '@rookcom/monarch-react';
ExportKindRenderingRoot class
EntityPickercomponentclient.mn-entity-picker-

Forwards its ref to the trigger button. Native button attributes go to the default trigger.

The panel is a Radix Popover (role="dialog") containing an inline Combobox and an optional footer.

Shares option, message and search types with Combobox (ComboboxOption, ComboboxMessages, ComboboxSearchContext).

Generated from the TypeScript declarations of packages/react/src/components/forms/entity-picker.

Properties

trigger replaces the default trigger; it must accept a ref and name itself, with its visible text in the name (for example aria-label="WEB: project Website refresh").

footer may be a function of { close, query }, for actions such as "Create “query”" or "Manage projects".

EntityPicker

Pick a record (a project, contact, company, team…) from a searchable list in a popover. The product supplies the records, search, rendering and creation; EntityPicker supplies the trigger, the popover, keyboard support and the single or multiple selection model. Built on Combobox (`inline`).

PropTypeDescription
align"center" | "start" | "end"Popover alignment against the trigger. Default `start`.
classNamestring
clearOptionComboboxClearOptionAn option at the top that clears the selection, such as "Unassigned" or "No project".
closeOnSelectbooleanClose after a pick. Default: true, or false with `multiple`.
contentClassNamestringClass on the popover panel.
defaultOpenboolean
defaultValuestring | null
defaultValuesstring[]
disabledboolean
emptyMessageReactNodeShown when nothing matches.
filter((option: ComboboxOption, query: string) => boolean)Custom filter for static `options`. Default: label, hint and keywords.
footerReactNode | ((context: EntityPickerFooterContext) => ReactNode)Content under the list, such as "Manage projects". A function gets `close` and the query.
idstring
invalidboolean
limitnumberRender at most this many matches, with a message to keep typing.
loadingbooleanShown while records load.
messagesPartial<ComboboxMessages>Interface text, for translation.
minSearchLengthnumberShortest query `onSearch` runs for. Default 0.
multiplebooleanPick several records; the list stays open after each pick.
onCreate((query: string) => ComboboxOption | null | undefined | Promise<ComboboxOption | null | undefined>)Adds a "Create “query”" option; return the new record to select it.
onOpenChange((open: boolean) => void)
onSearch((query: string, context: ComboboxSearchContext) => Promise<ComboboxOption[]>)Load records from a server; see Combobox `onSearch`. Debounced, aborted when superseded.
onValueChange((value: string | null) => void)
onValuesChange((values: string[]) => void)
openbooleanWhether the picker is open (controlled).
optionsComboboxOption[]The records to pick from. With `onSearch`, shown until the first search answers.
placeholderstringTrigger text when nothing is selected. Default "Select…".
renderOption((option: ComboboxOption, state: ComboboxOptionState) => ReactNode)Custom option content. Default: media, label, description, hint and a check mark.
renderValue((selected: ComboboxOption[]) => ReactNode)Content of the default trigger for the current selection. Default: media and label, or "first +N".
searchDebouncenumberMilliseconds to wait after typing before `onSearch`. Default 250.
searchLabelstringAccessible name of the search box. Default: `searchPlaceholder`.
searchPlaceholderstringPlaceholder of the search box. Default "Search…".
size"sm" | "md" | "lg"
sortByMatchbooleanRank matches whose label starts with the query first. Default true.
triggerReactElement<unknown, string | import("/home/runner/work/monarch/monarch/node_modules/@types/react/index").JSXElementConstructor<any>>A custom trigger element (a chip, an icon button). It must accept a ref and name itself, for example `aria-label="Status: Open"`. Default: a field-style button.
valuestring | nullSelected record id (controlled). Single-select only.
valueOptionsComboboxOption[]Records for the current value(s) when they may not be in `options` (async lists). Used for labels.
valuesstring[]

Variants

PropValuesDescription
size'sm''md''lg'Trigger height from --mn-control-*; the search row uses sm or md.
multiple'false''true'One record, or several shown as "first +N".
align'start''center''end'Popover alignment against the trigger.

States

StateExposed asBehaviour
Placeholderdata-placeholderNothing selected: the placeholder in muted text.
Openaria-expanded="true" on the triggerThe popover with search and list is shown.
Selectedaria-selected="true"Check mark on chosen records.
Loading / error / emptyrole="status"From Combobox: "Loading…", a retry after a failed search, or emptyMessage.
Invaliddata-invalid, aria-invalidDanger border, from invalid or the Field.
DisableddisabledThe trigger cannot open the picker.

Interaction behaviour

  • Clicking the trigger (or Enter / Space on it) opens the popover and focuses the search box.
  • Typing filters (or calls onSearch after the debounce); arrows move, Enter or a click picks.
  • Single: picking closes the popover and returns focus to the trigger. Multiple: picking toggles and the list stays open.
  • clearOption adds a first option such as "No project"; onCreate adds "Create “text”".
  • Escape or clicking outside closes without changing the value.

Keyboard support

KeysAction
Enter / Space / ArrowDown (trigger)Opens the picker.
ArrowDown / ArrowUpMoves the active record.
PageDown / PageUp, Home / EndMoves ten, or to the first or last record.
EnterPicks or toggles the active record.
EscapeCloses and returns focus to the trigger.
TabMoves to the footer actions, if any.

Accessibility

Role
select-only combobox (the trigger) opening a dialog that holds a search combobox and a listbox
Accessible name
The Field label or aria-label; the selected value is the trigger’s content.
  • The default trigger follows the APG select-only combobox: role="combobox", named by its label, with aria-expanded, aria-haspopup="dialog" and aria-controls; its text is the value.
  • The popover is a named dialog; focus moves into the search box and back to the trigger on close.
  • The search box is an APG combobox with aria-activedescendant; groups are labelled role="group".
  • With multiple the listbox is aria-multiselectable.

Responsive behaviour

  • The popover is at least 16rem wide (or the trigger width) and never wider than the viewport minus 2rem.
  • The trigger truncates long labels with an ellipsis; the list scrolls within 18rem.

Theme support

  • Trigger, panel and options use semantic tokens shared with Input and the menu styles; light, dark and more-contrast themes apply.
  • The popover entrance animation respects reduced motion.

Do and don't

Do

  • Use onSearch with the signal for server-backed records; pass valueOptions for selected records not in the results.
  • Put management actions ("Manage projects", "Invite people") in the footer.
  • Name a custom trigger with its value and keep its visible text in the name, such as "Open: status".

Don't

  • Do not build a second picker with checkboxes in a list; use multiple.
  • Do not put product fetching inside a wrapper that refetches on every render; memoise or cache in the product.
  • Combobox: The search and list it is built on; use directly for an input-style field.
  • AssigneePicker: People preset with avatars.
  • Select: Short lists without search.
  • Popover: The floating panel.

Dependencies

KindDepends on
npm packages@radix-ui/react-popoverreact
Monarch componentsCombobox, Field

The trigger uses .mn-input; the panel uses .mn-menu.

Testing coverage

  • Opens from a labelled trigger, focuses the search, picks with Enter, closes and refocuses the trigger.
  • Groups and prefix ranking; Escape closes without a change.
  • Multiple stays open, toggles and summarises as "first +N".
  • Clear option, footer with close and query, custom trigger.
  • Async search plus create keeps the picked label; disabled and invalid from Field.
CheckCovered
Axe in jsdomYes
Server renderingYes
Browser suitesVisual baselines: /__gallery/entity-picker (closed and open); Cross-browser axe: /components/entity-picker

Tests: packages/react/src/components/forms/entity-picker