Usage
@prepared911/ui/user-menu and @prepared911/ui/user-menu/styles.css. UserMenu renders other components, whose stylesheets are separate, so also load @prepared911/ui/avatar/styles.css, @prepared911/ui/badge/styles.css, @prepared911/ui/box/styles.css, @prepared911/ui/button/styles.css, @prepared911/ui/icon/styles.css, @prepared911/ui/menu/styles.css, @prepared911/ui/segmented-control/styles.css, @prepared911/ui/status-dot/styles.css and @prepared911/ui/text/styles.css, and the stylesheets Menu's usage page lists. The Reduce motion row needs no Switch stylesheet: its track is an internal part that menu/styles.css already holds.
UserMenu is the account menu behind a person's avatar in an app's top bar. The trigger is the avatar (photo or initials). A presence dot is optional and absent unless you pass a status. The panel leads with an identity header, then the account destinations, a preferences group, and Sign out.
triggerLabel is the accessible name of the trigger. Write it to include the person's name, for example "Dana Levi, account menu".
To open the menu from another control, such as the preferences icon button of a top bar, pass it as trigger instead and drop triggerLabel. The control keeps its own name; the menu adds the button attributes and returns focus to it on close. See the Navbar contract (NB-16).
showName the trigger is the avatar alone. With showName it also draws the name (one line, capped at 12rem) and a chevron that turns while the menu is open, from 640px of viewport width up. The accessible name is the same either way and the button element is the same, so a window that resizes across 640px keeps focus.avatarSrc. Without it, or when it fails to load, the avatar shows initials, which default to the first letter of each of the first two words of name.status (or defaultStatus, or onStatusChange) adds a presence dot to the avatar; without one nothing renders. See Availability.defaultOpen or a controlled open does not take focus.plan), and the email. The name and email truncate to one line and keep their full text as a title.items are the account destinations: label, optional icon, optional shortcut hint, onSelect(). Pressing one runs onSelect, closes the menu, and returns focus to the trigger. A shortcut is optional and a visual hint only (it is hidden from assistive technology); the app binds the key. See Shortcuts.align="end") and opens below it (side="block-end").packages/ui/src/components/menu/CONTRACT.md).| Row | Props | Reports |
|---|---|---|
| Theme | appearanceLabels, appearance, defaultAppearance, onAppearanceChange | AppearanceMode.Light, Dark or System |
| Reduce motion | reduceMotionLabel, reduceMotion, defaultReduceMotion, onReduceMotionChange | true or false |
| Availability | statusLabels, status, defaultStatus, onStatusChange | UserMenuStatus.Available, Busy or Away |
preferencesLabel puts the rows under a visible heading.
Every on/off preference row (Reduce motion today) is one menuitemcheckbox with aria-checked that draws the design system's Switch track at the inline end. The whole row toggles on a press, Enter or Space, and the menu stays open. The track is a picture: it has no input and no tab stop, so no control nests inside the row. An uncontrolled preference survives closing and reopening the menu.
The app owns the effect. A minimal wiring:
data-motion="reduced" on <html>, MotionPreference and applyMotionPreference()) is owned by the motion preference work; UserMenu does not import it. Pass details.cancel() in a change handler to refuse a change.
Availability is optional. The default menu has no status dot and no Available, Busy or Away rows. Passing status, defaultStatus or onStatusChange shows the Available, Busy and Away rows (and then statusLabels is required) and a presence dot on the avatar in the color of the current status. The dot is decorative: the rows carry visible labels and the trigger's accessible name should carry the status, so triggerLabel may be a function of the status.
Sign out is not a destructive action (owner ruling): it uses the neutral default item styling and takes no destructive intent, so it looks like any other row.
onSignOut may return a promise. While it is pending the row shows a spinner, reads signingOutLabel, is aria-busy, and ignores further presses; the menu closes when the promise settles, resolved or rejected, and focus returns to the trigger. A rejection is not caught: handle the error in onSignOut. A callback that returns nothing closes the menu at once, like any destination.
A destination takes an optional shortcut. None of the default examples set one, including Settings, but any item may:
open, defaultOpen and onOpenChange(open, details) follow the design system's controlled and uncontrolled rules. details.reason is "trigger-press", "item-press", "focus-out" or "light-dismiss".
Below 640px only the avatar shows and the panel stays a popover. A bottom sheet is a follow-up.