Skip to content

User Form

A form to create or edit a user, in three presentations: a whole page (variant page), a compact form for a Dialog (modal) and a single-column form for a Sheet (sheet). Fields: profile picture, name, job title, email, phone with a calling-code select, address, time zone, language, bio, role, active switch and team, chosen with a plain fields object. Edit mode adds a danger zone to suspend or reinstate and delete, each behind a confirmation. Save stays disabled until something changes, and Cancel asks before throwing changes away. Your onSubmit 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.

Create

Full screen (opens in a new tab)

New user

Profile

How this person appears to others.

PNG, JPEG, WebP or GIF, up to 2 MB.

Contact

Where we reach this person.

Calling code

Access

What this person can do and whether they can sign in.

RoleCan read everything, change nothing.

Suspended users cannot sign in.

Team
 

Edit

Full screen (opens in a new tab)

Edit Lena Fischer

Profile

How this person appears to others.

PNG, JPEG, WebP or GIF, up to 2 MB.

Contact

Where we reach this person.

Email cannot be changed here.
Calling code

Access

What this person can do and whether they can sign in.

RoleCan create and change records.

Suspended users cannot sign in.

Team

Danger zone

These actions affect this person's access to everything.

Suspend accessThey are signed out and cannot sign in until you reinstate them.
Delete userRemoves the user for good. This cannot be undone.
 

Permissions

Full screen (opens in a new tab)

Edit Priya Raman

Profile

How this person appears to others.

PNG, JPEG, WebP or GIF, up to 2 MB.

Contact

Where we reach this person.

Email cannot be changed here.
Calling code

Access

What this person can do and whether they can sign in.

Only owners can change roles.

Suspended users cannot sign in.

Danger zone

These actions affect this person's access to everything.

Suspend accessThey are signed out and cannot sign in until you reinstate them.
Only owners can suspend users.
Delete userRemoves the user for good. This cannot be undone.
Only owners can delete users.
 

Page

Full screen (opens in a new tab)

Edit Lena Fischer

Profile

How this person appears to others.

PNG, JPEG, WebP or GIF, up to 2 MB.

Contact

Where we reach this person.

Email cannot be changed here.
Calling code

Address

Used for invoices and shipping.

Country

Preferences

Time zone, language and a short introduction.

Time zone
Language
46 of 280 characters

Access

What this person can do and whether they can sign in.

RoleCan create and change records.

Suspended users cannot sign in.

Team

Danger zone

These actions affect this person's access to everything.

Suspend accessThey are signed out and cannot sign in until you reinstate them.
Delete userRemoves the user for good. This cannot be undone.
 

In a dialog

Full screen (opens in a new tab)

In a sheet

Full screen (opens in a new tab)

Minimal

Full screen (opens in a new tab)

Add a user

Role

Suspended users cannot sign in.

 

Installation

npx rdloom add user-form

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/user-form.json

Works, but without upgrade tracking.

Usage

import { UserForm } from "@rdloom/react";

<div className="flex w-full justify-center">
  <div className="w-full max-w-2xl">
    <UserForm
      mode="create"
      variant="page"
      layout="card"
      title="New user"
      fields={{ address: false, preferences: false, bio: false }}
      roles={roles}
      teams={teams}
      defaultValues={{ role: "viewer" }}
      onSubmit={createUser}
      onCancel={() => {}}
    />
  </div>
</div>

API Reference

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

PropTypeDefault
mode

create starts empty and shows no danger zone. edit makes the email read-only, with a hint, and shows the danger zone.

"create" | "edit""create"
variant

page is a whole page form: sections with the heading on the left and the fields on the right from the lg breakpoint, and a sticky Cancel and Save footer. modal is compact for a Dialog: name, email, role and status only, no danger zone. sheet is single column for a Sheet: avatar header, grouped sections, danger zone at the bottom and a footer pinned to the bottom of the sheet.

"page" | "modal" | "sheet""page"
fields

Which optional fields show. Defaults depend on the variant: page and sheet show all of them (team still needs teams, role still needs the changeRole permission); modal shows role and status only. Name and email always show.

Partial<Record<"avatar" | "jobTitle" | "phone" | "address" | "preferences" | "bio" | "team" | "role" | "status", boolean>>none
defaultValues

The starting values. Only read when the form mounts; give it a key to start over. phoneCountry is an id from the calling-code list (for example US, GB, DE, IN).

Partial<{ name: string; email: string; role: string; status: "active" | "suspended"; team: string | null; avatar: string | null; jobTitle: string; phoneCountry: string; phone: string; street: string; apartment: string; city: string; region: string; postalCode: string; country: string; timeZone: string; language: string; bio: string }>none
rolesrequired

The roles a user can have. The description of the chosen role is shown under its Select.

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

The teams a user can belong to. Without teams the team field is left out.

Array<{ id: string; label: string }>none
title

A heading for the form. Leave it out when the form sits in a Sheet or Dialog that already has a title.

nodenone
headingLevel

Heading level of the title, 1 to 6.

number2
layout

plain has no chrome, for a Sheet, a Dialog or a page that already has one. card draws a bordered card with padding around the form.

"card" | "plain""plain"
submitLabel

The text of the Save button. Default: "Create user" or "Save changes".

stringnone
successMessage

Announced and shown when onSubmit worked.

string"User saved."
dangerZone

More actions for the danger zone, shown next to Suspend and Delete user. Edit mode only, and never in the modal variant.

nodenone
onSubmitrequired

Saves the user; yours, async. Only the values of fields that are shown are sent. avatarFile holds the File when a new picture was chosen. Return { fieldErrors } to show a server problem on a field (for example email) or { formError } for the whole form.

(values: { name: string; email: string; role: string; status: "active" | "suspended"; team: string | null; avatar: string | null; avatarFile?: File | null; jobTitle: string; phoneCountry: string; phone: string; street: string; apartment: string; city: string; region: string; postalCode: string; country: string; timeZone: string; language: string; bio: string }) => void | SubmitResult | Promise<void | SubmitResult>none
onCancel

Called when the person cancels. With unsaved changes it is called after they confirm Discard.

() => voidnone
onDelete

Deletes the user; yours. The Delete user button asks first. Without it the button is left out.

() => void | Promise<unknown>none
onSuspend

Suspends the user right away (called with true), or reinstates one who is suspended (called with false); yours. The button asks first. Without it the button is left out.

(suspended: boolean) => void | Promise<unknown>none
permissions

What the app allows: edit (the whole form), suspend (Suspend and Reinstate; falls back to edit), delete (Delete user) and changeRole (the role field). Hidden removes it; disabled makes a field read-only but focusable with the reason as its description, and a button stays focusable with the reason. This only changes what people see: the server must check again.

Permissions<"edit" | "delete" | "suspend" | "changeRole">none
classNames

Extra class names for single parts, so you can restyle one part without editing the file. Keys: root, header, title, form, fields, section, avatar, actions, saveButton, cancelButton, dangerZone, status.

Partial<Record<"root" | "header" | "title" | "form" | "fields" | "section" | "avatar" | "actions" | "saveButton" | "cancelButton" | "dangerZone" | "status", string>>none
countries

The countries in the address country select. A short built-in list is used when left out.

Array<{ id: string; label: string }>none
timeZones

The time zones to choose from, as IANA ids. A short built-in list is used when left out.

Array<{ id: string; label: string }>none
languages

The languages to choose from. A short built-in list is used when left out.

Array<{ id: string; label: string }>none

Accessibility

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

Keyboard

  • Tab: moves through the profile picture buttons, the fields in reading order, the danger zone, then Cancel and Save
  • Enter or Space on Upload picture: opens the file chooser; Remove picture clears it
  • Enter in a text field: saves
  • Escape: with unsaved changes asks "Discard changes?"; without them it does nothing here
  • Delete user, Suspend and Reinstate open a confirmation that you answer with Tab and Enter

Screen readers announce

  • "Email, edit text, read only" with "Email cannot be changed here"
  • "Active, switch, on" with "Suspended users cannot sign in"
  • "Discard changes?, alert dialog"
  • "User saved." after a successful save
  • "Picture selected: portrait.png" after choosing a file
  • "Phone number, edit text" with a "Calling code" select before it

What your code must do

  • Save is disabled until a value changes
  • Cancel with unsaved changes opens an alert dialog; Keep editing returns focus to where it was
  • A failed save moves focus to the error summary; each item moves to its field
  • A field the person may not change is read-only, stays focusable, and its description gives the reason
  • The email in edit mode is read-only with a hint saying why
  • The danger zone is a labelled group, so it is announced as its own part of the page
  • The profile picture has an Upload and a Remove button; the result (chosen, removed, or why a file was refused) is announced in a status region
  • Pictures are checked for type (PNG, JPEG, WebP, GIF) and size (2 MB) before they are accepted
  • Phone numbers are checked for length and characters, the calling code is a labelled select
  • Address, phone and name fields carry autocomplete tokens (street-address, postal-code, country-name, tel-national)
  • The bio shows its character count and reports it in the field description

Block contract

Data
The user's current values (defaultValues), the roles and teams to choose from, and async onSubmit, onDelete and onSuspend.
Data states
ready
Permissions
edit, delete, suspend, changeRole
Events
onSubmit, onCancel, onDelete, onSuspend
You can replace
classNames for each part; dangerZone slot for extra actions; mode, variant, layout, title, submitLabel and successMessage; fields to choose what shows; roles, teams, countries, time zones and languages

Guidelines

Use it when

  • Creating a user, or editing one on a page, in a Sheet or in a Dialog

Avoid it when

  • Inviting several people by email: use InviteDialog
  • Editing your own profile and password: use a settings page

Don't

  • Treating the permissions prop as protection: UI permission is not security, so the server must check again
  • Hiding a field the person may see but not change: use disabled with a reason
  • Closing a Sheet around the form in a way that skips onCancel, which loses the Discard changes question
  • Putting variant="page" inside a small Dialog: use variant="modal" or sheet
  • Showing Suspend or Delete user in a Dialog that has its own buttons for them: the modal variant leaves them out

Design tokens

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

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