Components · Forms
EntityPicker
Pick one or several records (projects, contacts, teams) from a searchable list in a popover.
Project with groups, clear option and footer
import { useState } from 'react';
import { Button, EntityPicker, Field, FieldLabel } from '@rookcom/monarch-react';
import type { ComboboxOption } from '@rookcom/monarch-react';
// BoardIcon, GridIcon, ListIcon: demo icons from the docs site; use your own.
const makeProjects = (): ComboboxOption[] => [
{
value: 'website',
label: 'Website refresh',
hint: 'WEB',
group: 'Active',
media: <BoardIcon />,
},
{
value: 'mobile',
label: 'Mobile onboarding',
hint: 'MOB',
group: 'Active',
media: <ListIcon />,
},
{
value: 'billing',
label: 'Billing migration',
hint: 'BIL',
group: 'Active',
media: <GridIcon />,
},
{
value: 'research',
label: 'Customer research',
hint: 'RES',
group: 'Paused',
description: 'Paused until next quarter',
media: <ListIcon />,
},
];
function ProjectDemo() {
const [projects] = useState(makeProjects);
const [value, setValue] = useState<string | null>('mobile');
return (
<Field style={{ width: '100%', maxWidth: 300 }}>
<FieldLabel>Project</FieldLabel>
<EntityPicker
options={projects}
value={value}
onValueChange={setValue}
placeholder="Choose a project"
clearOption={{ label: 'No project' }}
footer={({ close }) => (
<Button variant="ghost" size="sm" block onClick={close}>
Manage projects
</Button>
)}
/>
</Field>
);
}
<ProjectDemo />Usage examples
Multiple with create
import { useState } from 'react';
import { EntityPicker, Field, FieldLabel } from '@rookcom/monarch-react';
import type { ComboboxOption } from '@rookcom/monarch-react';
const labels: ComboboxOption[] = [
{ value: 'bug', label: 'Bug' },
{ value: 'feature', label: 'Feature' },
{ value: 'docs', label: 'Documentation' },
{ value: 'perf', label: 'Performance' },
];
function LabelsDemo() {
const [values, setValues] = useState(['bug', 'perf']);
const [options, setOptions] = useState(labels);
return (
<Field style={{ width: '100%', maxWidth: 300 }}>
<FieldLabel>Labels</FieldLabel>
<EntityPicker
multiple
options={options}
values={values}
onValuesChange={setValues}
placeholder="Add labels"
onCreate={(q) => {
const option = { value: q.toLowerCase(), label: q };
setOptions((list) => [...list, option]);
return option;
}}
/>
</Field>
);
}
<LabelsDemo />Chip trigger
import { useState } from 'react';
import { Button, EntityPicker } from '@rookcom/monarch-react';
import type { ComboboxOption } from '@rookcom/monarch-react';
// BoardIcon, GridIcon, ListIcon: demo icons from the docs site; use your own.
const makeProjects = (): ComboboxOption[] => [
{
value: 'website',
label: 'Website refresh',
hint: 'WEB',
group: 'Active',
media: <BoardIcon />,
},
{
value: 'mobile',
label: 'Mobile onboarding',
hint: 'MOB',
group: 'Active',
media: <ListIcon />,
},
{
value: 'billing',
label: 'Billing migration',
hint: 'BIL',
group: 'Active',
media: <GridIcon />,
},
{
value: 'research',
label: 'Customer research',
hint: 'RES',
group: 'Paused',
description: 'Paused until next quarter',
media: <ListIcon />,
},
];
function ChipDemo() {
const [projects] = useState(makeProjects);
const [value, setValue] = useState<string | null>('website');
const current = projects.find((p) => p.value === value);
return (
<EntityPicker
options={projects}
value={value}
onValueChange={setValue}
size="sm"
trigger={
<Button
size="xs"
aria-label={`${current?.hint ?? 'None'}: project ${current?.label ?? 'none'}`}
>
{current?.hint ?? 'None'}
</Button>
}
/>
);
}
<ChipDemo />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
optionsor fromonSearch, and rich rows come frommedia,description,hintorrenderOption. - 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';| Export | Kind | Rendering | Root class |
|---|---|---|---|
EntityPicker | component | client | .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`).
| Prop | Type | Description |
|---|---|---|
align | "center" | "start" | "end" | Popover alignment against the trigger. Default `start`. |
className | string | |
clearOption | ComboboxClearOption | An option at the top that clears the selection, such as "Unassigned" or "No project". |
closeOnSelect | boolean | Close after a pick. Default: true, or false with `multiple`. |
contentClassName | string | Class on the popover panel. |
defaultOpen | boolean | |
defaultValue | string | null | |
defaultValues | string[] | |
disabled | boolean | |
emptyMessage | ReactNode | Shown when nothing matches. |
filter | ((option: ComboboxOption, query: string) => boolean) | Custom filter for static `options`. Default: label, hint and keywords. |
footer | ReactNode | ((context: EntityPickerFooterContext) => ReactNode) | Content under the list, such as "Manage projects". A function gets `close` and the query. |
id | string | |
invalid | boolean | |
limit | number | Render at most this many matches, with a message to keep typing. |
loading | boolean | Shown while records load. |
messages | Partial<ComboboxMessages> | Interface text, for translation. |
minSearchLength | number | Shortest query `onSearch` runs for. Default 0. |
multiple | boolean | Pick 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) | |
open | boolean | Whether the picker is open (controlled). |
options | ComboboxOption[] | The records to pick from. With `onSearch`, shown until the first search answers. |
placeholder | string | Trigger 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". |
searchDebounce | number | Milliseconds to wait after typing before `onSearch`. Default 250. |
searchLabel | string | Accessible name of the search box. Default: `searchPlaceholder`. |
searchPlaceholder | string | Placeholder of the search box. Default "Search…". |
size | "sm" | "md" | "lg" | |
sortByMatch | boolean | Rank matches whose label starts with the query first. Default true. |
trigger | ReactElement<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. |
value | string | null | Selected record id (controlled). Single-select only. |
valueOptions | ComboboxOption[] | Records for the current value(s) when they may not be in `options` (async lists). Used for labels. |
values | string[] |
Variants
| Prop | Values | Description |
|---|---|---|
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
| State | Exposed as | Behaviour |
|---|---|---|
| Placeholder | data-placeholder | Nothing selected: the placeholder in muted text. |
| Open | aria-expanded="true" on the trigger | The popover with search and list is shown. |
| Selected | aria-selected="true" | Check mark on chosen records. |
| Loading / error / empty | role="status" | From Combobox: "Loading…", a retry after a failed search, or emptyMessage. |
| Invalid | data-invalid, aria-invalid | Danger border, from invalid or the Field. |
| Disabled | disabled | The 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
onSearchafter 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.
clearOptionadds a first option such as "No project";onCreateadds "Create “text”".- Escape or clicking outside closes without changing the value.
Keyboard support
| Keys | Action |
|---|---|
| Enter / Space / ArrowDown (trigger) | Opens the picker. |
| ArrowDown / ArrowUp | Moves the active record. |
| PageDown / PageUp, Home / End | Moves ten, or to the first or last record. |
| Enter | Picks or toggles the active record. |
| Escape | Closes and returns focus to the trigger. |
| Tab | Moves 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, witharia-expanded,aria-haspopup="dialog"andaria-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 labelledrole="group". - With
multiplethe listbox isaria-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
onSearchwith the signal for server-backed records; passvalueOptionsfor 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.
Related components
- 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
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.
| Check | Covered |
|---|---|
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/entity-picker (closed and open); Cross-browser axe: /components/entity-picker |
Tests: packages/react/src/components/forms/entity-picker