Skip to content

Invite Dialog

A dialog for inviting people, in three shapes: typed emails with a role on one compact line each, a search of a directory you pass, or a two-step pick-from-a-list with a role per person. Checks for format, duplicates and people who are already members, an error summary, an optional message and a submit button that says how many invitations it sends. Your onInvite does the work. UI permission is not security: the server must check again.

Blockv0.1.0experimentalWCAG 2.2 AAView spec

A ready-made piece, built from the library's own parts.

A block puts several components together into something you would otherwise assemble by hand. It never fetches data: you give it the data, or answer its callbacks. It is copied into your project like any component, with the parts it uses, so you can change anything.

Basic

Full screen (opens in a new tab)

With server errors

Full screen (opens in a new tab)

Permissions

Full screen (opens in a new tab)

Many people

Full screen (opens in a new tab)

With message toggle

Full screen (opens in a new tab)

Search directory

Full screen (opens in a new tab)

Pick from list

Full screen (opens in a new tab)

Installation

npx rdloom add invite-dialog

Copies the source into src/components/rdloom. Edit it freely: rdloom upgrade merges later versions into your changes. It needs react-aria-components; add --install to install them.

Use another registry client
npx shadcn@latest add https://rdloom.vimalbhatt.com/r/invite-dialog.json

Works, but without upgrade tracking.

Usage

import { Button, DialogTrigger, InviteDialog } from "@rdloom/react";

<div className="flex w-full justify-center">
  <DialogTrigger>
    <Button>Invite people</Button>
    <InviteDialog
      roles={roles}
      defaultRole="viewer"
      existingEmails={["amara.okafor@example.com", "lena.fischer@example.com"]}
      description="They get an email with a link to join your workspace."
      onInvite={sendInvites}
    />
  </DialogTrigger>
</div>

API Reference

Defined by the spec. Components also accept the props of the React Aria component they wrap.

PropTypeDefault
variant

emails: one compact line per person, typed email and role. search: look people up in a directory you pass and add them to a to-invite list. list: a two-step modal, pick people from a list, then give each a role.

"emails" | "search" | "list""emails"
rolesrequired

The roles a person can be invited as. The description of a role shows in the open list of the role select.

Array<{ id: string; label: string; description?: string }>none
defaultRole

The id of the role each new row starts with. Defaults to the first role.

stringnone
existingEmails

Emails that are already members. Typing one is an error on that row. Compared without case.

string[]none
defaultInvites

Rows the emails variant starts with, for example people to invite again. Without it there is one empty row.

Array<{ email: string; role?: string }>none
people

The directory for the search and list variants. In search it is what the search looks in unless onSearch is given; in list it is every person that can be picked.

Array<{ id: string; name: string; email: string; avatarUrl?: string }>none
onSearch

Search variant only: looks people up on your server as the person types. Return the matches; the latest query wins. Without it, people is filtered in the browser.

(query: string) => Array<{ id: string; name: string; email: string; avatarUrl?: string }> | Promise<Array<{ id: string; name: string; email: string; avatarUrl?: string }>>none
maxVisibleRows

Rows shown before the list scrolls inside itself, with a fade at the edge that has more. The dialog never grows past 90% of the screen height.

number4
maxInvites

The most people one send can hold. Add another is disabled at the limit and says so.

number10
title

The dialog's title, shown as its heading. Default: "Invite people"; the list variant shows "Select users" and then "Give user role".

stringnone
description

One sentence under the title.

stringnone
allowMessage

Offers an optional message that goes with every invitation, behind an "Add a message" switch. Set false to remove it entirely. The list variant never has one.

booleantrue
messageLabel

The label of the message field.

string"Message (optional)"
submitLabel

The text of the submit button for a number of invitations. Default: "Send invitation" and "Send 3 invitations".

(count: number) => stringnone
successMessage

The status message announced once the dialog has closed after a send. Default: "3 invitations sent".

(count: number) => stringnone
onInviterequired

Sends the invitations; yours, async. person is set when someone was picked from the directory. Resolve for success. Return { fieldErrors: { "invites[1].email": "..." } } to show a problem on a row, or { formError } for the whole form; a throw also keeps the dialog open.

(invites: Array<{ email: string; role: string; person?: { id: string; name: string; email: string; avatarUrl?: string } }>, details: { message?: string }) => void | SubmitResult | Promise<void | SubmitResult>none
onCancel

Called when the person cancels: the Cancel button or Escape.

() => voidnone
onOpenChange

Called with false when the dialog closes after a cancel or a successful send. Needed with isOpen.

(isOpen: boolean) => voidnone
isOpen

Controlled open state, for a dialog opened without a DialogTrigger.

booleannone
permissions

What the app allows: invite. Hidden renders nothing; disabled keeps the dialog but the submit button stays focusable with aria-disabled and the reason shown and read, and nothing is sent. This only changes what people see: the server must check again.

Permissions<"invite">none
classNames

Extra class names for single parts, so you can restyle one part without editing the file. Keys: dialog, form, list, message, toggle, search, results, counter, actions, cancelButton, submitButton, reason, status.

Partial<Record<"dialog" | "form" | "list" | "message" | "toggle" | "search" | "results" | "counter" | "actions" | "cancelButton" | "submitButton" | "reason" | "status", string>>none

Accessibility

Role dialog, WCAG 2.2 AA. Tested with axe and keyboard tests; screen reader checks are in the audit checklist.

Keyboard

  • Tab: moves through the rows, Add another, the message switch, Cancel and the submit button; focus stays inside the dialog
  • Escape: cancels and closes
  • Enter in an email field: sends
  • Search variant: Down arrow from the search field moves into the results; Enter adds the person
  • List variant: Up and Down move between people, Space or Enter picks or drops one, Next goes to the roles step, Prev comes back

Screen readers announce

  • "Invite people, dialog"
  • "Email 2, edit text, required", then "Role 2"
  • "Select users, Choose people, list box, multiple selection"
  • "Send 3 invitations, button"
  • "3 invitations sent" after the dialog closes

What your code must do

  • Focus lands on the first email field when the dialog opens, and on a new row's email when one is added
  • After a send that worked the dialog closes and focus returns to the control that opened it
  • A failed send keeps the dialog open and moves focus to the error summary; each item moves to its field
  • Row errors are linked to their field with aria-describedby and aria-invalid
  • Every row's inputs keep a unique accessible name (Email 2, Role 2) even where the label is visually hidden
  • Adding and removing a row is announced; a status message tells the result after the dialog closes
  • The list variant moves focus to the new step's heading on Next and Prev and announces the step politely
  • A disabled invite stays focusable and the reason is its description

Block contract

Data
The roles people can be invited as, the emails of existing members, optionally a directory of people (static or from an async onSearch), and an async onInvite that does the sending.
Data states
ready
Permissions
invite
Events
onInvite, onCancel, onOpenChange
You can replace
classNames for each part; title, description, messageLabel, submitLabel and successMessage; roles, maxInvites, maxVisibleRows and allowMessage; variant emails, search or list; people and onSearch for a directory; works inside DialogTrigger or controlled with isOpen

Guidelines

Use it when

  • Inviting people to a team, workspace or account by email, one or many at a time

Avoid it when

  • Adding a user with a full profile: use UserForm
  • Importing hundreds of people: use a file import flow

Don't

  • Treating the permissions prop as protection: UI permission is not security, so the server must check again
  • Sending the invitations from inside the component: onInvite is yours
  • Closing the dialog yourself after a failed onInvite: it stays open on purpose so the person can fix the rows

Design tokens

The semantic tokens this component uses. Change them once and every component follows; see Design tokens.

  • --rd-color-surface-raised
  • --rd-color-border-default
  • --rd-color-text-default
  • --rd-color-text-muted
  • --rd-color-feedback-danger
  • --rd-color-focus-ring
  • --rd-radius-overlay
  • --rd-elevation-overlay