Member List
The people in a team, a space or a share list — an add row (picker · role · Add) and 36px rows with a role chip and a Remove menu that scroll after eight.
- Status
- Since
0.25.87- Accessibility pattern
- named list; role and ⋯ menus named per row
Last updated
- MKManoj Kumar (you)Owner
- Product team2 people
- SDSanjay Desai
- PSPriya Shah
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Member List before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify member-listInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/member-listRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/dropdown-menu, @vegastack/permission-menu, @vegastack/person-avatar, @vegastack/scroll-area, @vegastack/skeleton.
Usage
import { MemberList, MemberListAdd } from "@/components/ui/member-list";
import { PeoplePicker } from "@/components/ui/people-picker";
<MemberList
add={
<MemberListAdd
picker={
<PeoplePicker
multiple
value={chosen}
onValueChange={setChosen}
search={searchPeople}
/>
}
roles={ROLES}
role={role}
onRoleChange={setRole}
onAdd={addChosen}
disabled={chosen.length === 0}
/>
}
members={members}
roles={ROLES}
onRoleChange={changeRole}
onRemove={removeMember}
/>;One list for every "people in X": a team's members, a space's members, who an item is shared with,
an invite list. MemberListAdd on top is the picker, an optional role for the new members (a
bordered PermissionMenu that lines up with the picker) and
Add. Each row is 36px: the PersonAvatar, the name over a muted sub line ("2 people" for a
team, "Creator"), the role as a chip menu, and a ⋯ menu with Remove. After maxRows (eight) the list scrolls inside itself. Data-agnostic: rows, roles and
every string come in through props and every change goes out through a callback.
ShareDialog is built on it.
Scope
- Owns: the add row's layout, the rows, the role chip and ⋯ menus, locked rows, the loading and empty states, the scroll cap.
- Does not own: searching people (the picker is the host's), what a role means, or saving.
- Compose with:
PeoplePickerin the add row; aDropdownMenuItemper extra row action throughmenuItems.
Anatomy
Examples
Add, change, remove
A built-in row (the owner, the creator) is locked: its role shows with a lock and
lockedReason in a tooltip, and it has no ⋯ — but the ⋯ column keeps its width on every row, so
the role chips line up. you adds "(you)" after the viewer's name. Narrower than 28rem, the add
row puts the role and Add under the picker.
- MKManoj Kumar (you)Owner
- Product team2 people
- SDSanjay Desai
- PSPriya Shah
Read-only
canManage={false} shows every role as text and drops the menus. Without roles, a row's role
is plain text — a team's "Lead" and "Member", say.
- MKManoj Kumar (you)OwnerFull access
- Product team2 peopleFull access
- SDSanjay DesaiCan edit
- PSPriya ShahCan view
- SDSanjay DesaiLead
- PSPriya ShahMember
Long lists and more actions
Past maxRows the rows scroll inside the list (a whole number of 36px rows tall). menuItems
adds the host's own items to a row's ⋯ menu, above Remove — "Resend invite" for an invited person;
removable: false keeps a row's menu without Remove.
API Reference
MemberList
| Prop | Type | Default | Description |
|---|---|---|---|
members* | readonly MemberListMember[] | — | The rows, in order. |
add | React.ReactNode | — | The add row above the list — a MemberListAdd. |
canManage | boolean | true | The viewer may change the list: role menus and ⋯ menus show. Without it, roles are text and there are no menus. |
empty | React.ReactNode | labels.empty, muted | What shows with no rows; null shows nothing. |
labels | Partial<MemberListLabels> | {} | Override any rendered string. |
loading | boolean | false | Rows are loading: skeleton rows show. |
maxRows | number | 8 | Rows shown before the list scrolls inside itself. |
menuItems | ((member: MemberListMember) => React.ReactNode) | — | More items for a row's ⋯ menu, above Remove — DropdownMenuItems ("Resend invite"). |
onRemove | ((id: string) => void) | — | Removes one row; adds Remove to its ⋯ menu. |
onRoleChange | ((id: string, role: string) => void) | — | Changes one row's role; without it every role reads as text. |
roles | readonly PermissionMenuOption<string>[] | — | The roles a row can have, most access first. With them each row's role is a chip menu;
without them a row's role is plain text. |
Data attributes and CSS variables on MemberList
| Attribute | Values |
|---|---|
data-slot | "member-list" | "member-list-description" | "member-list-empty" | "member-list-more" | "member-list-more-spacer" | "member-list-name" | "member-list-role" | "member-list-row" | "member-list-rows" | "member-list-scroll" |
Each row of members:
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | A stable id — the person's or the team's. |
person* | Person | — | The person, team (kind: "team") or agent. |
description | React.ReactNode | — | The muted sub line under the name — "2 people" for a team, "Creator", "via Sales team". |
locked | boolean | false | A built-in row (the creator, an owner): the role shows with a lock and lockedReason in a
tooltip, and the row has no ⋯ menu. |
lockedReason | React.ReactNode | — | Why a locked row cannot change, in its tooltip — "Set by role: Creator". |
removable | boolean | true | Offer Remove on this row (when the list has onRemove). |
role | string | — | Their role — one of the list's roles, or plain text when the list has none. |
roleReadOnly | boolean | false | Show this row's role as text, with no menu, even for a manager. |
you | boolean | false | This row is the viewer: "(you)" follows the name. |
MemberListAdd
| Prop | Type | Default | Description |
|---|---|---|---|
picker* | React.ReactNode | — | The picker that chooses who to add — a PeoplePicker, say. It fills the row. |
adding | boolean | false | An add is in flight: Add spins, and the role control waits. |
addLabel | string | "Add" | The Add button's text. |
disabled | boolean | false | Nothing to add yet (no one chosen): Add is disabled. |
onAdd | (() => void) | — | Adds the chosen people. |
onRoleChange | ((role: string) => void) | — | Changes the role for the new members; without it the role reads as text. |
role | string | the last of `roles` | The role the new members get — one of roles. |
roleLabel | ((role: string) => string) | (role) => `Role for new members: ${role}` | The role control's accessible name, with the visible role in it. |
roles | readonly PermissionMenuOption<string>[] | — | The roles the new members can get. Omit for no role control. |
Data attributes and CSS variables on MemberListAdd
| Attribute | Values |
|---|---|
data-slot | "member-list-add" | "member-list-add-button" | "member-list-add-picker" |
Accessibility
- The rows are a list named by
labels.list("Members"); a loading list isaria-busy. - Every role menu is named for its row with the visible role in it ("Priya Shah's role: Can view"), and every ⋯ button for its row ("More for Priya Shah").
- A locked role is still a tab stop, with its reason read out and shown in a tooltip.
- The sub line is its own line of text, read after the name.
| Key | Action |
|---|---|
| Tab | Moves through the add row, then each row's menus |
| Enter / Space | Opens a role or ⋯ menu |
| ArrowUp / ArrowDown | Moves through an open menu |
| Escape | Closes the open menu |
| Contract | States tested |
|---|---|
| Behaviour | default, loading, empty, locked, read-only, scrolling, adding |
| Accessibility | labeled, keyboard, browser-accessibility-test |
| Visual | default, hover, focus, open, locked, read-only |