Skip to content
Component installs need the registry setup
VegaStack Design

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
stable
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-input

The 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 — search returns only addable people and teams.
  • Compose with: PermissionMenu for the batch's level and share-01 for 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

PropTypeDefaultDescription
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-labelstring"Add people or teams"The text input's accessible name, when no label names it.
classNamestring—Classes for the chips field.
disabledbooleanfalseDisable the field and its chips.
emptyTextstring"No people or teams found"Shown when a search finds nobody.
idstring—The text input's id, for a <label htmlFor>.
loadingTextstring"Searching…"Shown while a search is in flight.
placeholderstring"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

AttributeValues
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.

PropTypeDefaultDescription
id*string—A stable id, unique across people and teams.
name*string—The name shown, and the avatar's initials.
badgeReact.ReactNode—A status after the name, such as "Inactive" — a string is a small muted outline badge.
emailstring—The muted line under the name, and the initials when there is no name; omit for someone without an account.
hueAvatarHue—The person's colour behind their initials — one of the ten tag hues; none is the muted fallback.
imagestring—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.
KeyAction
TypeSearch
↑ / ↓Move through results
EnterAdd the highlighted person or team
BackspaceIn the empty field, remove the last chip
←From the start of the field, move to chips
EscClose the results
ContractStates tested
Behaviourempty, searching, results, no-results, error, chips, remove, backspace-remove, disabled, in-dialog, in-sheet
Accessibilitylabeled, keyboard, browser-accessibility-test
Visualdefault, focus, open, chips

Do / Don't

Do
Leave out people who already have access, or who cannot be added, in search.
Don't
Put an access level on each chip — pick one level for the batch beside the field.
Do
Use PeopleInput to add several people and teams from a directory.
Don't
Use it for free-form addresses (ChipInput) or for picking a single person (SearchableSelect).

On this page