People Input
Add people or teams as removable chips, searched as you type — a Combobox chips field over the host's async search, listing person and team rows.
- Status
- Since
0.23.112- Accessibility pattern
- chips combobox; Backspace removes the last chip
Last updated
Install
Add People Input from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/people-inputThe same command installs the registry items it composes: @vegastack/button, @vegastack/combobox, @vegastack/person-avatar, @vegastack/searchable-select, @vegastack/spinner, @vegastack/use-async-search.
Usage
import {
PeopleInput,
type PeopleInputOption,
} from "@/components/ui/people-input";
const [invitees, setInvitees] = React.useState<PeopleInputOption[]>([]);
<PeopleInput
value={invitees}
onValueChange={setInvitees}
search={(query, { signal }) => searchDirectory(query, signal)}
/>;"Add people or teams…": type, and the host's search(query) returns matching people and teams
(debounced, and race-safe through useAsyncSearch — a slow answer never overwrites a newer one).
Each row is a PersonOption over a PersonAvatar; a team (kind: "team") shows its
rounded-square tile. A pick becomes a chip with a remove button, and Backspace in the
empty field removes the last chip. A chip has no access level of its own — a share invite picks one
level for the whole batch.
Scope
- Owns: the chips field, the search state (debounce, abort, loading, empty, error), the person and team rows and chips.
- Does not own: the directory or who may be added —
searchreturns only addable people and teams. - Compose with:
PermissionMenufor the batch's level andshare-01for the whole dialog.
Examples
Chosen people and teams
A team chip carries the team icon; a person chip the person icon.
Disabled and no results
disabled while an invite sends; emptyText when a search finds nobody.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
onValueChange* | (value: PeopleInputOption[]) => void | — | Called with the new list when a chip is added or removed. |
search* | (query: string, context: { signal: AbortSignal; }) => Promise<PeopleInputOption[]> | — | Finds people and teams for a query ("" when the field opens empty). Called debounced; the
signal aborts when a newer query starts. Leave out anyone who cannot be added. |
value* | readonly PeopleInputOption[] | — | The chosen people and teams, in the order they were added. |
aria-label | string | "Add people or teams" | The text input's accessible name, when no label names it. |
className | string | — | Classes for the chips field. |
disabled | boolean | false | Disable the field and its chips. |
emptyText | string | "No people or teams found" | Shown when a search finds nobody. |
id | string | — | The text input's id, for a <label htmlFor>. |
loadingText | string | "Searching…" | Shown while a search is in flight. |
placeholder | string | "Add people or teams…" | The field's placeholder while no chip is chosen. |
removeLabel | ((option: PeopleInputOption) => string) | (option) => `Remove ${option.name}` | The remove button's accessible name for one chip. |
Data attributes and CSS variables on PeopleInput
| Attribute | Values |
|---|---|
data-kind | "person" |
data-people-input | "" |
data-slot | "people-input-chip-remove" |
PeopleInputOption
A Person (from person-avatar) with a stable id; kind: "team" draws it as a team.
| Prop | Type | Default | Description |
|---|---|---|---|
id* | string | — | A stable id, unique across people and teams. |
name* | string | — | The name shown, and the avatar's initials. |
badge | React.ReactNode | — | A status after the name, such as "Inactive" — a string is a small muted outline badge. |
email | string | — | The muted line under the name, and the initials when there is no name; omit for someone without an account. |
hue | AvatarHue | — | The person's colour behind their initials — one of the ten tag hues; none is the muted fallback. |
image | string | — | The avatar image. |
kind | "person" | "team" | "person" | A person, or a team of people — a team is a rounded-square tile with the team icon on its hue. |
Accessibility
- The text input is an editable combobox named "Add people or teams" (override with
aria-label, or label it with a<label htmlFor={id}>); the list is a named multi-select listbox. - Each chip's remove button is named
Remove {name}(removeLabel). - "Searching…" and errors are announced through the combobox's own polite status region.
- The popup portals above a Dialog or a bottom Sheet and is never clipped by them.
| Key | Action |
|---|---|
| Type | Search |
| ↑ / ↓ | Move through results |
| Enter | Add the highlighted person or team |
| Backspace | In the empty field, remove the last chip |
| ← | From the start of the field, move to chips |
| Esc | Close the results |
| Contract | States tested |
|---|---|
| Behaviour | empty, searching, results, no-results, error, chips, remove, backspace-remove, disabled, in-dialog, in-sheet |
| Accessibility | labeled, keyboard, browser-accessibility-test |
| Visual | default, focus, open, chips |