Axon 911 Frontend Docs
    Loading navigation…
U
User
Switch to light mode
  1. Components
  2. User Menu
  3. Usage

User Menu

Usage

Import @prepared911/ui/user-menu from its subpath. Load @prepared911/ui/styles.css once at the document root. UserMenu is the account menu behind a person's avatar in an app's top bar, and every app draws the same one: the Figma account menu. The trigger is the avatar (photo or initials). The panel leads with an identity header, then a rule, an Appearance section (Theme, Reduce motion, Reset layout, Zoom), a Sound section (Headset volume, Application sound), a rule, and Sign out. The web app leaves out Zoom (showZoom={false}); everything else is the same as the desktop app.
The Props DesktopDefault and WebDefault stories show the two versions. 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).
Trigger
  • Without 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.
  • The photo comes from 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.
  • A press toggles the menu. ArrowDown on the trigger opens it on the first row and ArrowUp on the last. Focus moves into the menu only when a person opens it: a menu that opens through defaultOpen or a controlled open does not take focus.
Panel
  • The header shows a larger avatar (photo or initials), the name, and the email below it. The name and email truncate to one line and keep their full text as a title.
  • The design has no account destinations. items is an optional slot, off by default, for extra account actions such as Diagnostics for a super admin: each has id, label, optional icon and disabled, and onSelect(). The rows sit after the last rule, just before Sign out; a press runs onSelect, closes the menu and returns focus to the trigger. Destinations and submenus belong in the app's own Menu.
  • The panel aligns to the end of the trigger by default (align="end") and opens below it (side="block-end").
  • Row highlights are concentric with the panel's rounded corners, in both writing directions: the panel is Menu's surface, and its rows take Menu's row radius (MN-33 in packages/ui/src/components/menu/CONTRACT.md).
  • Every option takes Menu's one gliding highlight, the Theme row and slider rows included: the pointer glides it between rows, arrow keys jump it, it jumps under reduced motion, and it hides under forced colors in favor of the focused row's edge. The header takes none.
Preferences
Preference rows keep the menu open. A row is drawn when the app passes its copy and pairs a controlled value with its change handler, and UserMenu only reports the choice: the app applies it. appearanceSectionLabel and soundSectionLabel put each section under its visible heading; a section with no rows is not drawn, so an app with no sound settings shows no Sound section.
RowSectionPropsReports
ThemeAppearanceappearanceLabels, appearance, defaultAppearance, onAppearanceChangeAppearanceMode.Light, Dark or System
Reduce motionAppearancereduceMotionLabel, reduceMotion, defaultReduceMotion, onReduceMotionChangetrue or false
ZoomAppearanceshowZoom, zoomLabel, zoom, onZoomChangePercent from 80 to 150. Row only when showZoom is true
Headset volumeSoundheadsetVolumeLabel, headsetVolume, onHeadsetVolumeChangeInteger from 1 to 15
Application soundSoundapplicationSoundLabel, applicationSound, onApplicationSoundChangeInteger from 0 to 15
Reset layoutAppearance, after Reduce motiononResetLayout, resetLayoutLabel, resetLayoutButtonLabelHost callback only. The row shows its label and an outlined Reset chip whenever onResetLayout is set; the whole row is one menuitem, so a press, Enter or Space runs it and the menu stays open. UserMenu does not confirm; the host confirms or offers undo before applying the reset
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. Each slider row matches the Figma layout: the row label and a numeric readout (percent for Zoom, plain number for volume and sound) sit above the track. UserMenu uses Slider with showValue inside a full-width MenuControlRow (Menu MN-46). The slider is one stop of the menu (Slider SL-26): vertical arrows move between rows; horizontal arrows and Page Up and Page Down change the value. Set showZoom from the shell when the Zoom row should appear, for example const showZoom = useCurrentPlatform()?.runtime === "desktop". UserMenu does not inspect the user agent.
Applying the preferences
The app owns the effect. A minimal wiring:
The reduce motion contract (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. Wire zoom, headset volume and application sound to your shell or media settings the same way.
Sign out
Sign out is not a destructive action (owner ruling): it uses the neutral default item styling, with its leading icon, 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.
Open state
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".
Small screens
Below 640px only the avatar shows and the panel stays a popover. A bottom sheet is a follow-up.
Previous
Tree / Accessibility
Next
User Menu / API and Development
On this page
Trigger
Panel
Preferences
Applying the preferences
Sign out
Open state
Small screens