A three-step flow that invites people to a workspace (emails, roles and an optional link) beside the workspace's members table, which fills in as each good email is entered. Built on Onboarding Shell, the Onboarding core step engine, Field, Avatar, Badge, Segmented and Select.
When to use
- Straight after a workspace is created: it takes the
Workspacethat Workspace Setup hands toonInvite. - When people paste lists of addresses, and a bad one should be caught and fixed before anything is sent.
- When showing who is about to join, and with which role, is worth more than a bare textarea.
When not to use
- Inviting one person from a settings page: a Field with an Input and a Select in a Dialog is enough.
- Managing people who are already members: a table with role Selects and a menu per row.
- A different set of questions: build your own flow on Onboarding Shell.
Installation
npx shadcn add @eased-pro/team-inviteNeeds the @eased-pro registry and your Pro token in components.json. Create a token and copy the setup from your account .
Usage
import { TeamInvite } from "@/components/eased/team-invite/team-invite";export function InviteTeam({ workspace }: { workspace: Workspace }) { const router = useRouter(); const joined = useAcceptedInvites(workspace.slug); return ( <TeamInvite className="h-dvh" workspace={workspace} members={[{ name: user.name, email: user.email, role: "Owner" }]} onSend={({ invites, link }) => api.sendInvites(workspace.slug, invites, link)} createLink={() => api.createInviteLink(workspace.slug)} joined={joined} onDone={() => router.push(`/${workspace.slug}`)} /> );}Examples
After Workspace Setup
Show Team Invite when Workspace Setup's success screen hands over: the workspace's name, URL and colour come with it.
const [workspace, setWorkspace] = useState<Workspace | null>(null);return workspace ? <TeamInvite workspace={workspace} onSend={send} /> : <WorkspaceSetup onCreate={create} onInvite={setWorkspace} />;Drive it from outside
useTeamInvite returns the controller the component would make. Hold it yourself to prefill, script or test the flow.
const setup = useTeamInvite({ workspace, onSend });setup.add("maya@fernhill.co, theo@fernhill.co");<TeamInvite setup={setup} />API reference
TeamInvite
| Prop | Type | Default | Description |
|---|---|---|---|
workspace | InviteWorkspace | Lumen | { name, url, color? }. Workspace Setup's Workspace fits as it is. |
members | TeamMember[] | you, as Owner | { name, email, role }: the first rows of the table. An address already here is flagged. |
defaultRole | "member" | "admin" | "guest" | "member" | The role a new chip starts with. |
onSend | (result: InviteResult) => void | Promise | Runs on Send. A promise shows the button loading; a rejection stays on the last step with its message. | |
createLink | (workspace) => string | Promise<string> | <url>/join/<random> | Makes the invite link. A rejection leaves the button to try again. |
joined | string[] | [] | Emails that have accepted: their Pending badge settles to Joined. |
onDone | (result: InviteResult) => void | The success screen's Go to button. | |
initial | Partial<InviteDraft> | Prefill, e.g. { text: "maya@fernhill.co" }. | |
persist | StepsPersist | off | Save progress across reloads. |
setup | TeamInviteController | From useTeamInvite; the props above then go to the hook instead. | |
manageFocus | boolean | (() => boolean) | true | See Onboarding Shell. |
className | string | "" | Give it a height. |
InviteResult is { workspace, invites: { email, role }[], link }. Only good addresses are sent.
Also exported: useTeamInvite, MembersPreview, MembersBadge, checkEntries, problemOf, nameOf and INVITE_ROLES.
Keyboard
| Key | Action |
|---|---|
Enter | In the email field with text, makes a chip; in an empty field, continues. |
, or Space | Makes a chip of what is typed. |
Backspace | In an empty email field, takes the last chip back into the field to edit. |
Escape | Goes back a step. |
| Arrow keys | Move between roles in the Everyone control. |
Accessibility
- The email field is labelled; a bad address's reason ("theo@fernhill needs a domain ending, like .com") is the field's error, read as it appears.
- A red chip is a button named with its reason ("theo@fernhill: needs a domain ending, like .com. Edit"); every chip has a Remove button.
- Each person's role Select is named for them ("Role for maya@fernhill.co").
- The members table is decorative (
aria-hidden): every value it shows is in the form. - Focus moves to each step's heading, and to "3 invites sent" at the end.
Motion
The signature is a row landing. Each good chip drops 8px into the members table on smooth, under a brand tint that fades as it settles; rows pasted together land 35ms apart, and the count beside Members rolls. The table's height springs to fit. A role change swaps in its cell, and a Pending badge settles to Joined with a colour cross-fade and a text swap. In the form, chips pop in on snappy and leave fast, the field shakes once when an address is flagged, and the link card springs open to the link. Reduced motion: rows and chips fade, nothing moves.
Notes for AI
Use it after a workspace is created, with the workspace from Workspace Setup's onInvite. Wire onSend to your invite API, createLink to your link API, and joined to accepted invites from your realtime channel or a poll.
Don't parse or validate emails yourself; the hook splits pasted lists and flags bad, repeated and existing addresses. Don't wrap it in a card or dialog with its own padding: it is a full surface, so give it a height and let it fill.
Related
- Workspace Setup: the step before, which hands over the workspace.
- Onboarding Shell: the frame and step engine under this flow.