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

Skip to content

Home

Spending is on track this month.

Follows the page theme and corners
'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

Spending

Follows the page theme and corners
'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 name without it.

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 isCollapsed unless given.

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.

EnterSpaceDownOn the trigger, open the menu with focus on its first item.
UpOn the trigger, open the menu with focus on its last item.
DownUpHomeEndA-ZMove between the items.
RightOn Theme, open its submenu. Left in right to left.
EscapeClose the open level, with focus back on its trigger.
TabClose 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.

esc

↑ ↓ to move↵ to open