The person's picture and name in the rail's foot, opening their account's choices.
In the rail
In a Sidebar's footer the trigger fills the row: the person's avatar, their name and a detail under it, such as an email or a workspace, and a chevron. The name and the detail wrap and never push the rail wider. The menu opens upward from the rail's foot, and its corner follows the rail's, as the rail's items do. On a phone the rail is a drawer and the trigger sits in its footer the same way.
Example
Example
Home
Spending is on track this month.
'use client';
import {
AccountMenu,
AppShell,
MenuItem,
MenuSeparator,
ThemeMenu,
type ThemeChoice,
} from '@snapshot-ui/react';
import { LifeBuoy, LogOut, Settings } from 'lucide-react';
import { useState, type ReactElement } from 'react';
import { ShellHeader, ShellNav, ShellPage } from './shell-nav';
/** The signed-in person's menu: Settings, the theme as a submenu, Help, a rule and Sign out. */
export function AdaMenu({ isCompact }: { isCompact?: boolean }): ReactElement {
const [theme, setTheme] = useState<ThemeChoice>('system');
return (
<AccountMenu name="Ada Lovelace" detail="ada@example.com" isCompact={isCompact}>
<MenuItem id="settings" href="#settings" icon={<Settings />}>
Settings
</MenuItem>
<ThemeMenu value={theme} onChange={setTheme} />
<MenuItem id="help" href="#help" icon={<LifeBuoy />}>
Help
</MenuItem>
<MenuSeparator />
<MenuItem id="sign-out" icon={<LogOut />}>
Sign out
</MenuItem>
</AccountMenu>
);
}
/** In the rail's footer, opening upward; collapse the rail and it is the avatar alone. */
export default function AccountMenuRail(): ReactElement {
return (
<div className="stage-shell">
<AppShell
sidebar={<ShellNav label="Main, with the account menu" footer={<AdaMenu />} />}
header={<ShellHeader />}
>
<ShellPage />
</AppShell>
</div>
);
}Folded and in a header
When the rail folds, the trigger is the avatar alone, with the name in a tooltip; it is the same element, so a rail that folds while the menu is open still has a trigger to give focus back to. A server that reads the rail's state from a cookie draws the folded face from the first paint. In an app with no rail, pass isCompact and put the trigger at the header's end, where its menu opens downward at its end.
Example
Example
Spending
'use client';
import { Heading } from '@snapshot-ui/react';
import type { ReactElement } from 'react';
import { AdaMenu } from './account-menu-rail';
/** An app with no rail: the avatar alone at the header's end, opening downward at its end. */
export default function AccountMenuHeader(): ReactElement {
return (
<div
style={{
width: 'min(40rem, 100%)',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: 'var(--spacing-peer)',
}}
>
<Heading level={3} size={4}>
Spending
</Heading>
<AdaMenu isCompact />
</div>
);
}With the theme
Put a ThemeMenu among the items for a Theme item that opens a submenu of System, Light and Dark, or pass display="section" for the three in the menu itself. See Theme switch for keeping the choice.
Usage
import { AccountMenu, MenuItem, MenuSeparator, Sidebar, ThemeMenu } from '@snapshot-ui/react';
<Sidebar
aria-label="Main"
footer={
<AccountMenu name="Ada Lovelace" detail="ada@example.com" avatarSrc="/ada.png">
<MenuItem id="settings" href="/settings">
Settings
</MenuItem>
<ThemeMenu value={theme} onChange={choose} />
<MenuItem id="help" href="/help">
Help
</MenuItem>
<MenuSeparator />
<MenuItem id="sign-out" onAction={signOut}>
Sign out
</MenuItem>
</AccountMenu>
}
>
...
</Sidebar>;Inside a Sidebar it follows the rail's collapsed state unless you pass isCompact. className, ref and a menu's other props, such as onAction, go on the menu.
Props
avatarSrc string | The avatar's image; initials from |
|---|---|
children ReactNode | MenuItem, MenuSection, MenuSeparator and ThemeMenu elements. |
defaultOpen boolean | Uncontrolled: whether it opens drawn open. Default: false |
detail ReactNode | A second line on the trigger, in slate: an email, a workspace. It describes the trigger. |
isCompact boolean | Avatar only, the name in a Tooltip and the trigger's name. Inside a Sidebar it follows the rail's
Default: false |
isOpen boolean | Controlled: whether the menu is open. |
menuLabel string | The menu's name. Default: 'Account' |
name string | The person's name: on the trigger, and the trigger's name. |
onOpenChange ((isOpen: boolean) => void) | |
placement Placement | Where the menu opens. Default: 'top start' in a Sidebar, 'bottom end' elsewhere |
Accessibility
The trigger is a button named by the person's name, the words you see, so a voice user can say it; the detail describes it. "Account" alone would hide whose account it is. Folded, the name is still its name and shows in a tooltip. The menu is named "Account" (menuLabel). Choosing an item closes the menu and gives focus back to the trigger, unless the item navigates, where the destination's own focus rule applies.
| EnterSpaceDown | On the trigger, open the menu with focus on its first item. |
|---|---|
| Up | On the trigger, open the menu with focus on its last item. |
| DownUpHomeEndA-Z | Move between the items. |
| Right | On Theme, open its submenu. Left in right to left. |
| Escape | Close the open level, with focus back on its trigger. |
| Tab | Close the whole menu and move on from the trigger. |
Design rules
What keeps the account's menu in the system.
Show the name the person knows, and a detail that tells two accounts apart.
Open it upward from the rail's foot.
Keep Sign out last, after a rule.
Never name the trigger Account alone.
Never draw Sign out as danger: it loses nothing.
Never put the theme choice inside a form.