Skip to content

User Menu

The avatar button that opens the account menu: the person's name and email at the top, your own entries (settings, billing, help) in groups, a row for the theme, and a Sign out entry that can ask first, shows that it is working, and tells screen readers how it went.

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)

Sign out flow

Full screen (opens in a new tab)

Signed in

Permissions

Full screen (opens in a new tab)

ClassNames

Full screen (opens in a new tab)

Installation

npx rdloom add user-menu

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-menu.json

Works, but without upgrade tracking.

Usage

import { SettingsIcon, UserIcon, UserMenu } from "@rdloom/react";

<div className="flex justify-center p-8 pb-48">
  <UserMenu
    user={{ name: "Ada Lovelace", email: "ada@example.com" }}
    items={[
      { id: "profile", label: "Your profile", icon: <UserIcon />, href: "/profile" },
      { id: "settings", label: "Settings", icon: <SettingsIcon />, href: "/settings" },
    ]}
    onSignOut={async () => {
      // Call your sign-out API here.
    }}
  />
</div>

API Reference

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

PropTypeDefault
userrequired

The signed-in person: name (also the accessible name), email and the picture. Without a picture the initials are shown.

{ name: string; email?: string; avatarUrl?: string }none
items

Entries above Sign out: { id, label, icon?, href?, onSelect?, permission?, variant? }. An entry with an href is a link; without one it is a button that calls onSelect. permission is "allow", "disabled", "hidden" or { state, reason }: hidden entries are not drawn, disabled ones are shown dimmed with the reason beside them.

UserMenuItem[]none
groups

More entries in groups, each with an optional heading. A separator is drawn between groups, and before Sign out.

UserMenuGroup[]none
themeRow

A row under the entries for choosing the theme, for example your segmented control. It sits below the menu inside the same popover and is reached with Tab.

nodenone
onSignOut

Adds the Sign out entry and does the work. May be async. While it runs the entry shows it is pending; a throw or rejection is an error. Call your own sign-out API here; the library does not.

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

Ask before signing out. Opens an alert dialog, for example { title: "Sign out of Loomworks?" }; its confirm button runs onSignOut.

{ title: string; description?: string; confirmLabel?: string }none
signOutLabel

Text of the Sign out entry.

string"Sign out"
signingOutLabel

Text of the entry while the sign-out is running.

string"Signing out..."
signedOutMessage

Announced politely when the sign-out succeeds. It is spoken from a status region that stays on the page after the menu closes.

string"You have signed out"
errorMessage

Announced politely when the sign-out fails.

string"Could not sign out. Try again."
onSignedOut

Called after a successful sign-out, with what onSignOut returned. Usually where you go to the sign-in page.

(result: unknown) => voidnone
onError

Called with the error when onSignOut throws or rejects.

(error: unknown) => voidnone
permissions

What the person may do: { signOut } as "allow" (default), "disabled" or "hidden", or { state, reason }. The server must check again.

Permissions<"signOut">none
size

Size of the avatar button.

"sm" | "md""sm"
align

Which edge of the button the menu lines up with.

"start" | "end""end"
showName

Show the name and email beside the picture in the button. Use it where there is room, such as a sidebar.

booleanfalse
label

Start of the button's accessible name. The person's name is added: "Account menu, Ada Lovelace".

string"Account menu"
classNames

Class names for the parts, by slot name, added after the built-in ones. Slots: root, trigger, avatar, name, popover, header, list, item, separator, theme, status.

Partial<Record<"root" | "trigger" | "avatar" | "name" | "popover" | "header" | "list" | "item" | "separator" | "theme" | "status", string>>none

Accessibility

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

Keyboard

  • Enter, Space or Arrow Down on the button: opens the menu
  • Arrow keys: move between entries; Home and End: first and last
  • Enter or Space: chooses an entry
  • Escape: closes the menu and returns focus to the button
  • Tab (with a theme row): moves from the menu to the controls in the row

Screen readers announce

  • "Account menu, Ada Lovelace, menu button, collapsed"
  • "Ada Lovelace, menu", then each entry: "Settings, menu item"
  • "Signing out..." while pending, then "You have signed out" or "Could not sign out. Try again." as a status message

What your code must do

  • The button is named with the person's name ("Account menu, Ada Lovelace"); the picture is decorative
  • The menu is a real menu named by the person's name; the entries are menu items or links
  • Closing the menu returns focus to the button
  • Sign out: while it runs the entry says so and a second choice is ignored; the result is announced politely from a status region that stays after the menu closes, not by moving focus
  • With confirm, the alert dialog traps focus, starts on the safe choice, and focus returns to the button when it closes
  • A disabled entry says why in text beside it, not only in a tooltip
  • A destructive entry is shown in the danger color and also says what it does in words

Block contract

Data
The signed-in person (name, email, picture) and the menu entries as plain data.
Data states
None: it shows what it is given
Permissions
signOut, per menu item
Events
onSignOut, onSignedOut, onError, item onSelect
You can replace
themeRow slot; items and groups; classNames for each part; size, align, showName

Guidelines

Use it when

  • The top-right corner of an app: who is signed in, account entries and Sign out
  • At the bottom of a sidebar, with showName

Avoid it when

  • A menu of actions on a record: use Menu
  • Switching organisations or workspaces: use the team switcher of Sidebar

Don't

  • Doing the sign-out yourself in onSelect of an item: use onSignOut so it gets pending, the message and the confirmation
  • Putting more than about 7 entries in the menu
  • UI permission is not security: hiding or disabling an entry only changes what people see, so the server must check again for every request

Design tokens

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

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