Source: https://monarchui.dev/components/assignee-picker/

Components · Identity

# AssigneePicker

People picker with avatars, "you" first, Unassigned, Assign to me and an avatar stack.

@rookcom/monarch-react

Component

## Assignee with you and Unassigned

## Usage examples

### Several reviewers

## Purpose

Assigns one or several people (owners, assignees, reviewers) from a searchable list of avatar rows. It is EntityPicker preset for people: the product passes its members, Monarch handles avatars, ordering, the shortcuts and the trigger summary.

### Use when

- Assigning a ticket, issue, task or alert to a person.
- Choosing reviewers, watchers or members (`multiple`).

### Avoid when

- Records that are not people: use EntityPicker.
- Mentioning people inside text: use a mention menu in the editor.

## Design principles

- People are recognised by face and name: every row has an Avatar, with presence when the product knows it.
- The most likely choices come first: Unassigned, then you, then the product’s order (groups such as "In this team").
- The product owns the people: members come in as `people` or from `onSearch`; nothing is fetched by Monarch.

## API reference

```tsx
import { AssigneePicker } from '@rookcom/monarch-react';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `AssigneePicker` | component | client | `.mn-assignee-picker` |

Accepts every EntityPicker prop except `options`, `onSearch`, `valueOptions`, `clearOption`, `onCreate` and `renderValue`, which it builds from people.

Forwards its ref to the trigger button. Values are person ids; Unassigned is `null` (single) or `[]` (multiple).

Generated from the TypeScript declarations of `packages/react/src/components/identity/assignee-picker`.

## Properties

`AssigneePerson` is `{ id, name, email?, avatarUrl?, status?, hint?, group?, disabled? }`.

### `AssigneePicker`

People picker: assign one or several people from a searchable list with avatars, the current user first, an "Unassigned" option and an "Assign to me" shortcut. The trigger shows the assignee, or an avatar stack for several. Built on EntityPicker and Avatar; the product supplies the people.

| Prop | Type | Description |
| --- | --- | --- |
| `align` | `"center" \| "start" \| "end"` | Popover alignment against the trigger. Default `start`. |
| `allowUnassigned` | `boolean` | Offer an "Unassigned" option that clears the selection. Default true. |
| `className` | `string` |  |
| `closeOnSelect` | `boolean` | Close after a pick. Default: true, or false with `multiple`. |
| `contentClassName` | `string` | Class on the popover panel. |
| `currentUserId` | `string \| null` | The signed-in user: listed first (within their group) as "(you)", with an "Assign to me" shortcut. |
| `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. |
| `maxAvatars` | `number` | Avatars shown in the trigger before a "+N" tile, with `multiple`. Default 3. |
| `messages` | `(Partial<AssigneePickerMessages> & 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. |
| `onOpenChange` | `((open: boolean) => void)` |  |
| `onSearch` | `((query: string, context: ComboboxSearchContext) => Promise<AssigneePerson[]>)` | Search people on a server instead of filtering `people`. Debounced and aborted like Combobox `onSearch`. |
| `onValueChange` | `((value: string \| null) => void)` |  |
| `onValuesChange` | `((values: string[]) => void)` |  |
| `open` | `boolean` | Whether the picker is open (controlled). |
| `people` | `AssigneePerson[]` | The people to choose from, such as workspace members. |
| `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. |
| `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…". |
| `showAssignToMe` | `boolean` | Show the "Assign to me" footer shortcut when `currentUserId` is set. Default true. |
| `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. |
| `valuePeople` | `AssigneePerson[]` | People for the current value(s) when they may not be in `people`. Used for names and avatars. |
| `values` | `string[]` |  |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `multiple` | `'false'``'true'` | One assignee with avatar and name, or several as an avatar stack and a count. |
| `size` | `'sm'``'md'``'lg'` | Trigger height, as EntityPicker. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Unassigned | `data-placeholder` | No one assigned: "Unassigned" in muted text. |
| Assigned | – | The trigger shows the avatar and name ("(you)" for the current user). |
| Several | – | An AvatarGroup (up to `maxAvatars`, then +N) and "N people". |
| Open | `aria-expanded="true"` | The people list with search. |
| Invalid / disabled | `data-invalid, disabled` | From `invalid`, `disabled` or the Field. |

## Interaction behaviour

- Search matches name and email.
- The current user (`currentUserId`) is listed first as "Name (you)"; "Assign to me" in the footer assigns them in one click.
- "Unassigned" (on by default, `allowUnassigned={false}` to hide) clears the assignment.
- Single closes after a pick; multiple toggles people and stays open.
- With `onSearch`, people are searched on the server with debounce and abort, as in Combobox.

## Keyboard support

| Keys | Action |
| --- | --- |
| Enter / Space / ArrowDown (trigger) | Opens the picker. |
| ArrowDown / ArrowUp, Home / End | Moves through people. |
| Enter | Assigns (or toggles) the active person. |
| Tab | Reaches "Assign to me" in the footer. |
| Escape | Closes and returns focus to the trigger. |

## Accessibility

Role

select-only combobox opening a dialog with a search combobox and listbox

Accessible name

The Field label or

aria-label

; the current assignee is the trigger’s content.

- Avatars in rows and the trigger are decorative; the name (and "(you)") carries the meaning.
- Presence is shown on row avatars; give status in `hint` too when it matters, as colour alone is not enough.
- Inherits EntityPicker’s dialog, combobox and listbox semantics.

## Responsive behaviour

- The list is at least 16rem wide; names and emails truncate.
- The trigger keeps the avatar stack and truncates the text.

## Theme support

- Avatars fall back to initials on seeded colours; everything else uses semantic tokens in all themes.

## Do and don't

### Do

- Pass `currentUserId` so people can assign themselves quickly.
- Group people the way the product thinks ("In this team", "Everyone else") with `group`.
- Pass `valuePeople` when the assignee may not be in the loaded list.

### Don't

- Do not use a native select of members; it loses search and avatars.
- Do not put the person’s availability only in the presence dot colour.

## Related components

- [EntityPicker](https://monarchui.dev/components/entity-picker/): The generic record picker it is built on.
- [Avatar](https://monarchui.dev/components/avatar/): The avatar and avatar stack.
- [Combobox](https://monarchui.dev/components/combobox/): Input-style search field.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Avatar](https://monarchui.dev/components/avatar/), [EntityPicker](https://monarchui.dev/components/entity-picker/) |

## Testing coverage

- Lists Unassigned, then you, then everyone with avatars; search matches email; picks and updates the trigger.
- "Assign to me" assigns and disappears once assigned; Unassigned clears.
- Multiple stays open and summarises as an avatar stack with a count.
- Server search keeps the picked name; valuePeople and translated messages.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/assignee-picker; Cross-browser axe: /components/assignee-picker |

Tests: `packages/react/src/components/identity/assignee-picker`

## All example code

### Assignee with you and Unassigned

```tsx
import { useState } from 'react';
import { AssigneePicker, Field, FieldLabel } from '@rookcom/monarch-react';
import type { AssigneePerson } from '@rookcom/monarch-react';

const members: AssigneePerson[] = [
  {
    id: 'm1',
    name: 'Priya Raman',
    email: 'priya@example.com',
    hint: '4 open',
    group: 'In this team',
  },
  {
    id: 'm2',
    name: 'Jonas Weber',
    email: 'jonas@example.com',
    status: 'online',
    group: 'In this team',
  },
  { id: 'm3', name: 'Amara Okafor', email: 'amara@example.com', group: 'Everyone else' },
  {
    id: 'm4',
    name: 'Lucas Martin',
    email: 'lucas@example.com',
    status: 'away',
    group: 'Everyone else',
  },
  { id: 'm5', name: 'Mei Chen', email: 'mei@example.com', group: 'Everyone else' },
];

function SingleDemo() {
  const [value, setValue] = useState<string | null>('m2');
  return (
    <Field style={{ width: '100%', maxWidth: 300 }}>
      <FieldLabel>Assignee</FieldLabel>
      <AssigneePicker people={members} currentUserId="m3" value={value} onValueChange={setValue} />
    </Field>
  );
}

<SingleDemo />
```

### Several reviewers

```tsx
import { useState } from 'react';
import { AssigneePicker, Field, FieldLabel } from '@rookcom/monarch-react';
import type { AssigneePerson } from '@rookcom/monarch-react';

const members: AssigneePerson[] = [
  {
    id: 'm1',
    name: 'Priya Raman',
    email: 'priya@example.com',
    hint: '4 open',
    group: 'In this team',
  },
  {
    id: 'm2',
    name: 'Jonas Weber',
    email: 'jonas@example.com',
    status: 'online',
    group: 'In this team',
  },
  { id: 'm3', name: 'Amara Okafor', email: 'amara@example.com', group: 'Everyone else' },
  {
    id: 'm4',
    name: 'Lucas Martin',
    email: 'lucas@example.com',
    status: 'away',
    group: 'Everyone else',
  },
  { id: 'm5', name: 'Mei Chen', email: 'mei@example.com', group: 'Everyone else' },
];

function MultipleDemo() {
  const [values, setValues] = useState(['m1', 'm2', 'm4', 'm5']);
  return (
    <Field style={{ width: '100%', maxWidth: 300 }}>
      <FieldLabel>Reviewers</FieldLabel>
      <AssigneePicker
        multiple
        people={members}
        currentUserId="m3"
        values={values}
        onValuesChange={setValues}
        allowUnassigned={false}
        placeholder="Add reviewers"
      />
    </Field>
  );
}

<MultipleDemo />
```
