Team
Team members and invites for an AI product's workspace settings. A seat meter that previews the invites you're typing, an invite box that turns a pasted list into checked chips, members with roles, search, filters and sorting, pending invites with resend cooldowns and copyable links, ownership transfer with typed confirmation, and removal with undo.
Every AI product with workspaces ends up with the same page: who's on the team, what each person can do, who's been invited and how many seats are left. Team is that page as one component you drop into a settings section. It reads your members, invites and seat count, and calls your handlers when someone acts. It never changes the data on its own, so what it shows is always what your server says.
The invite box takes addresses the way people actually have them. Type and press comma, space or Enter, or paste a column from a spreadsheet, a To: line from an email or a list with names like Ana Lima <[email protected]>. Each address becomes a chip. Duplicates are dropped, words that aren't addresses are skipped when you paste, and chips for invalid addresses, existing members and people who already have an invite are flagged with the reason, plus a button to remove them all at once. Pick the role, add an optional note, and send.
The seat meter shows members, pending invites and the chips you're typing as three layers, so you see the cost of an invite before you send it. Expired invites don't hold a seat. When the invites would go past the limit, sending is blocked with how many to remove, and at the limit the meter turns into an Add seats call to action.
Each member row has the person's role as a menu with what each role can do. The guard rails are built in: the only owner can't demote themselves or leave until they transfer ownership, and the menus say so instead of quietly hiding the option. Admins can't touch owners. Making someone an owner or lowering your own role asks first. Transferring ownership asks you to type the new owner's email, so the wrong Ana never gets the keys.
Pending invites show when they were sent, when they're about to expire and when they did. Resend renews the link and starts a cooldown counted from the last send, so the countdown survives a reload. Copy invite link puts the real link on the clipboard. Removing a member asks once, then offers Undo, and focus moves to the next person instead of falling back to the top of the page.
Add the Pro registry to components.json
components.json Add your token
Create a token on your account page and put it in
.env.localasHEXTAUI_PRO_TOKEN.Add the block
pnpm dlx shadcn@latest add @hextaui-pro/team
Wire it to your API
TeamSettings is controlled: pass members, invites and seats, and update them in each handler. Return { email: message } from onInvite to keep the addresses your server refused, with the reason on each chip.
Only what you allow
Each action appears only when its handler is set, and only for people who may take it. Without onInvite there's no invite box, and members and viewers see the list without menus.
Undo a removal
Pass onRestore and the toast after a removal gets an Undo button that hands you the removed member to add back.
Anatomy
The parts you compose, from the outside in.
| Part | Description |
|---|---|
TeamSettings | The whole page: seats, the invite box, pending invites, members and the optional domain auto-join row. Place it inside a SettingsSection. |
parseEmailList | The parser behind the invite box. Splits pasted or typed text into lowercase addresses, unwraps Name <email> and mailto: forms, and on paste skips words that aren't addresses. |
teamRoles | The four roles with their labels and descriptions, in order from most to least access. |
| Prop | Type | Default |
|---|---|---|
members{ id, name, email, role, avatarUrl?, lastActiveAt?, joinedAt? }. lastActiveAt null shows Not yet active. | TeamMember[] | – |
invites{ id, email, role, sentAt, expiresAt, link? }. Times accept a Date, an ISO string or milliseconds. link enables Copy invite link. | TeamInvite[] | [] |
currentUserIdThe signed-in member. Their role decides which actions show up. | string | – |
seatsSeats on the plan. Members and unexpired invites use one each. Leave it out for unlimited seats. | number | – |
domainShows a row that lets people with a verified company email join without an invite. | { name, verified, autoJoin, role? } | – |
resendCooldownSeconds after an invite is sent before it can be sent again. | number | 60 |
pageSizeHow many members show before Show more. | number | 50 |
onInviteAdds the invite box. Return messages by email for addresses your server refused; they stay as chips with the message. Throw to keep every chip and show the error on the button. | ({ emails, role, note }) => void | { [email]: message } | Promise | – |
onRoleChangeAdds the role menu. The row shows a spinner until it resolves, and a toast if it throws. | (memberId, role) => void | Promise | – |
onRemoveAdds Remove from team, and Leave team on your own row. If it throws, the dialog stays open with the message. | (memberId) => void | Promise | – |
onRestoreAdds Undo to the toast after a removal, with the member as it was. | (member) => void | Promise | – |
onTransferOwnershipAdds Transfer ownership for owners, confirmed by typing the new owner's email. | (memberId) => void | Promise | – |
onResendAdds Resend. Update sentAt and expiresAt so the cooldown starts and the link renews. | (inviteId) => void | Promise | – |
onRevokeAdds Revoke invite. | (inviteId) => void | Promise | – |
onAutoJoinChangeMakes the domain switch work. It shakes and stays put if this throws. | (enabled) => void | Promise | – |
onUpgradeAdds an Add seats button, which becomes the main action when every seat is taken. | () => void | – |
upgradeHrefMakes Add seats a link instead. | string | – |
classNameClasses for the wrapper, a column of groups. | string | – |
parseEmailList
Returns { emails, skipped }.
| Prop | Type | Default |
|---|---|---|
textAnything with addresses in it, separated by commas, semicolons, spaces, tabs or new lines. | string | – |
options.pasteSkip words without an @ when at least one address is present, and count them in skipped. | boolean | false |
| Key | Action |
|---|---|
| Enter | In the invite box, turns what you typed into a chip. Press it again on an empty box to send. |
| , | Also turns what you typed into a chip, as do space and semicolon. |
| Backspace | On an empty invite box, moves to the last chip. Backspace or Delete there removes it and moves to the one before. |
| ⌘Enter | Sends the invites from the address box or the note. Ctrl+Enter on Windows and Linux. |
| Escape | Clears the member search, or closes an open menu or dialog. |
- Member rows are list items named by the person. Role menus are named like "Admin, change role for Ana Lima" and the more button like "More actions for Ana Lima".
- Chips that can't be sent carry their reason in their name, and the list of problems under the box is attached to the input as its description.
- Adding addresses, sending, role changes and removals are announced politely in one live region. Errors in dialogs are announced as alerts.
- Disabled actions explain themselves: the role menu and the more menu say why the only owner can't change their role or leave, and Resend stays focusable during its cooldown with the time left in its name.
- After a removal, focus moves to the next member's actions, or the previous one at the end of the list. Revoking an invite does the same for invites.
- The seat meter is a meter with its label and the count as text. With reduced motion it changes without sliding.
Code
3 files, added to components/blocks/team.