Source: https://monarchui.dev/components/member-row/

Components · Identity

# MemberRow

One member of a workspace or space: avatar, name, email, role, status (invited, active, suspended) and confirmed actions.

@rookcom/monarch-react/patterns

Component

## Members with editable roles

## Usage examples

### Invited, suspended and read-only

## Purpose

The same member row everywhere people are listed with a role: workspace and space members, channel members, project and team members. The product passes the person, the roles allowed and callbacks; who may change what stays in the product. Import from `@rookcom/monarch-react/patterns`.

### Use when

- A list of people with a role that some viewers may change.
- Pending invitations shown alongside members, with resend and revoke.

### Avoid when

- Choosing people to add: use EntityPicker or AssigneePicker.
- Dense tables with many columns per person: use DataTable with Avatar cells.

## Design principles

- Identity first: avatar, name and email read as one unit; everything else is secondary.
- Only what differs is marked: invited and suspended get a badge, active members do not, so a long list stays quiet.
- Every control is named after the person (“Role for Jonas Weber”, “Remove Jonas Weber”), so a list of identical buttons is still clear to screen-reader users.
- Destructive actions are confirmed in a ConfirmDialog that can show what will happen.
- Async by default: role changes and resends show a busy state; failures are explained in the row and roll back.

## API reference

```tsx
import { MemberList, MemberRow } from '@rookcom/monarch-react/patterns';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `MemberList` | component | client | `.mn-member-row` |
| `MemberRow` | component | client | `.mn-member-row` |

MemberRow renders an `<li>` (ref) by default, or a `<div>` with `as="div"`; MemberList is the `<ul>` that holds rows.

The member’s role is `memberRole` / `defaultMemberRole` / `onMemberRoleChange` (not the ARIA `role`), chosen from `roleOptions`.

`onMemberRoleChange`, `onResend` and `onRemove` may return promises; a rejected role change restores the previous role.

Data attributes: `data-status` (`invited`, `active`, `suspended`) and `data-self`.

Generated from the TypeScript declarations of `packages/react/src/components/identity/member-row`.

## Properties

### `MemberList`

The list that holds MemberRows, so screen readers announce "list, 12 items".

| Prop | Type | Description |
| --- | --- | --- |
| `children` | `ReactNode` |  |
| `className` | `string` |  |
| `id` | `string` |  |

### `MemberRow`

One member of a workspace, space or channel: avatar, name, email, role, status (invited, active, suspended) and actions. Role changes, resends and removals go through product callbacks; removal is confirmed first. Use inside `MemberList`.

| Prop | Type | Description |
| --- | --- | --- |
| `name` required | `string` | Display name. Also seeds the avatar initials and colour. |
| `actions` | `ReactNode` | More trailing actions, such as a DropdownMenu. Name them after the member. |
| `as` | `"div" \| "li"` | Element to render. Default `li`, for use inside `MemberList`. |
| `avatarUrl` | `string \| null` | Avatar image URL; initials are shown without it or when it fails. |
| `badges` | `ReactNode` | Extra markers after the name, such as a Guest badge. |
| `canChangeRole` | `boolean` | Show the role select. Default: when `onMemberRoleChange` is given and it is not your own row. |
| `canRemove` | `boolean` | Show Remove. Default: when `onRemove` is given and it is not your own row. |
| `className` | `string` |  |
| `defaultMemberRole` | `string` | Initial member role (uncontrolled). |
| `disabled` | `boolean` | Disable every control in the row. |
| `email` | `ReactNode` | Email or handle, shown under the name. |
| `id` | `string` |  |
| `isSelf` | `boolean` | The row is the signed-in person: shows "You" and hides Remove. |
| `memberRole` | `string` | The member's role (controlled), one of `roleOptions`' values. Not the ARIA `role`. |
| `messages` | `Partial<MemberRowMessages>` | Override visible and announced strings. |
| `meta` | `ReactNode` | A line of extra detail under the email: joined date, invite expiry, availability. |
| `onMemberRoleChange` | `((role: string) => unknown)` | Makes the role a select. Return a promise to show it busy; reject to restore the previous role and show the error in the row. |
| `onRemove` | `(() => unknown)` | Remove the member (or revoke the invitation), after a confirmation dialog. Return a promise to keep the dialog pending; reject to show the error there. |
| `onResend` | `(() => unknown)` | Resend an invitation. Shown for `status="invited"`. |
| `presence` | `AvatarStatus` | Presence dot on the avatar. |
| `removeConsequences` | `ReactNode[]` | Consequences listed in the remove dialog ("3 assigned tickets go back to the queue"). |
| `roleOptions` | `readonly NativeSelectOption[]` | The roles the product allows here. Their labels also show for read-only roles. |
| `status` | `MemberStatus` | Default `active`. `invited` and `suspended` show a badge and change the actions. |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `status` | `'active'``'invited'``'suspended'` | `invited` shows an Invited badge, Resend and Revoke; `suspended` a Suspended badge and a muted name. |
| `as` | `'li'``'div'` | `li` (default) inside MemberList; `div` for a single row in a card or popover. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| Active | `data-status="active"` | No status badge. |
| Invited | `data-status="invited"` | Info badge; Resend invite; Remove becomes Revoke invite. |
| Suspended | `data-status="suspended"` | Warning badge and a muted name. |
| Self | `data-self` | “(You)” after the name; role select and Remove hidden by default. |
| Role read-only | – | Without `onMemberRoleChange` (or with `canChangeRole={false}`) the role label is text. |
| Busy | `aria-busy on the select or button` | While a role change or resend runs. |
| Error | `role="alert" in the row` | A failed role change or resend, in the product’s words. |
| Disabled | – | Every control in the row disabled. |

## Interaction behaviour

- Changing the role calls `onMemberRoleChange`. Uncontrolled, the select updates at once and rolls back if the promise rejects; controlled, it waits for the product’s new `memberRole`.
- Resend calls `onResend` and announces “Invite sent again.”
- Remove opens a danger ConfirmDialog (title, description and optional consequences) and calls `onRemove`; errors stay in the dialog so the person can retry.
- Extra actions (for example a DropdownMenu) go in `actions`, before Remove.

## Keyboard support

| Keys | Action |
| --- | --- |
| Tab | Moves between the role select, Resend, extra actions and Remove. |
| Arrow keys | Change the role in the native select. |
| Enter / Space | Activates Resend or Remove. |
| Escape | Closes the remove confirmation. |

## Accessibility

Role

listitem (in MemberList, a list)

Accessible name

Rows are named by their text; controls are labelled after the member.

- MemberList is a real `<ul>`, so screen readers announce “list, n items”.
- The avatar is decorative next to the visible name; presence is read as hidden text after the name (“Online”).
- Role select: “Role for {name}”. Remove: “Remove {name}” or “Revoke invite for {name}”. Resend: “Resend invite, {name}”.
- Resend success is a polite status; failures are `role="alert"`.
- Verified with axe in jsdom and in Chromium, Firefox and WebKit on /components/member-row.

## Responsive behaviour

- Name and email truncate with an ellipsis instead of pushing the controls off screen.
- When the row is narrower than 26rem (container query), the controls move under the name, aligned with the text.
- Buttons keep their touch size at every width.

## Theme support

- Monarch tokens only: names in `--mn-color-text-default`, emails in `--mn-color-text-muted`, row separators `--mn-color-border-subtle`.
- Badges use the info and warning tones; follows light, dark and high-contrast themes.

## Do and don't

### Do

- Pass only the roles the viewer may assign in `roleOptions`; enforce the same rule on the server.
- Add `removeConsequences` when removing someone affects their work (“3 open tickets go back to the queue”).
- Use `meta` for one useful fact: joined date, invite expiry or availability.

### Don't

- Do not let people remove themselves from this row; offer “Leave” elsewhere.
- Do not put the role in a menu when there are only a few roles; the native select is faster.
- Do not show “Active” on every row; it is the default.

## Related components

- [Avatar](https://monarchui.dev/components/avatar/): The avatar used here; on its own for compact people chips.
- [AssigneePicker](https://monarchui.dev/components/assignee-picker/): Choose people to assign or add.
- [DataTable](https://monarchui.dev/components/data-table/): Many columns per person, sorting and bulk actions.
- [ConfirmDialog](https://monarchui.dev/components/confirm-dialog/): The removal confirmation.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [Button](https://monarchui.dev/components/button/), [Badge](https://monarchui.dev/components/badge/), [Field](https://monarchui.dev/components/form-field/), [ConfirmDialog](https://monarchui.dev/components/confirm-dialog/), [Avatar](https://monarchui.dev/components/avatar/) |

## Testing coverage

- Identity, presence text, badges, email, meta and read-only role in a list; self row; axe.
- Labelled role select changes the role (uncontrolled); axe.
- A rejected role change restores the previous role and shows the error.
- Controlled role waits for the product and is busy meanwhile.
- Own row hides role select and Remove by default.
- Resend calls `onResend` and announces success; axe.
- Remove by keyboard opens a confirm dialog with consequences, focus on Cancel; rejection shown in the dialog, retry succeeds.
- Invited rows revoke; suspended rows are marked.
- Disabled, `as="div"` and translated messages; renders on the server.

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

Tests: `packages/react/src/components/identity/member-row`

## All example code

### Members with editable roles

```tsx
import { useState } from 'react';
import { MemberList, MemberRow } from '@rookcom/monarch-react/patterns';

const ROLES = [
  { value: 'owner', label: 'Owner' },
  { value: 'admin', label: 'Admin' },
  { value: 'member', label: 'Member' },
];

const wait = () => new Promise((r) => setTimeout(r, 700));

function TeamDemo() {
  const [people, setPeople] = useState([
    { id: '1', name: 'Priya Raman', email: 'priya@example.com', role: 'owner', self: true },
    { id: '2', name: 'Jonas Weber', email: 'jonas@example.com', role: 'admin' },
    { id: '3', name: 'Amara Okafor', email: 'amara@example.com', role: 'member' },
  ]);
  return (
    <MemberList aria-label="Workspace members" style={{ width: '100%' }}>
      {people.map((p) => (
        <MemberRow
          key={p.id}
          name={p.name}
          email={p.email}
          isSelf={p.self ?? false}
          roleOptions={ROLES}
          memberRole={p.role}
          onMemberRoleChange={async (role) => {
            await wait();
            setPeople((all) => all.map((x) => (x.id === p.id ? { ...x, role } : x)));
          }}
          onRemove={async () => {
            await wait();
            setPeople((all) => all.filter((x) => x.id !== p.id));
          }}
        />
      ))}
    </MemberList>
  );
}

<TeamDemo />
```

### Invited, suspended and read-only

```tsx
import { Badge } from '@rookcom/monarch-react';
import { MemberList, MemberRow } from '@rookcom/monarch-react/patterns';

const ROLES = [
  { value: 'owner', label: 'Owner' },
  { value: 'admin', label: 'Admin' },
  { value: 'member', label: 'Member' },
];

const wait = () => new Promise((r) => setTimeout(r, 700));

<MemberList aria-label="People" style={{ width: '100%' }}>
  <MemberRow
    name="lee@example.com"
    status="invited"
    meta="Invite expires in 6 days"
    roleOptions={ROLES}
    memberRole="member"
    onResend={wait}
    onRemove={wait}
  />
  <MemberRow
    name="Sam Carter"
    email="sam@example.com"
    status="suspended"
    roleOptions={ROLES}
    memberRole="member"
  />
  <MemberRow
    name="Mei Tanaka"
    email="mei@example.com"
    presence="online"
    badges={<Badge size="sm">Guest</Badge>}
    meta="Joined 12 March"
    roleOptions={ROLES}
    memberRole="admin"
  />
</MemberList>
```
